pyrestkit 1.2.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.
Files changed (115) hide show
  1. pyrestkit/__init__.py +35 -0
  2. pyrestkit/ai/__init__.py +17 -0
  3. pyrestkit/ai/analyzer.py +137 -0
  4. pyrestkit/ai/client.py +101 -0
  5. pyrestkit/ai/config/__init__.py +5 -0
  6. pyrestkit/ai/config/ai_config.py +200 -0
  7. pyrestkit/ai/exceptions.py +22 -0
  8. pyrestkit/ai/generators/__init__.py +0 -0
  9. pyrestkit/ai/models.py +44 -0
  10. pyrestkit/ai/parsers/__init__.py +0 -0
  11. pyrestkit/ai/prompts/failure_analysis.md +21 -0
  12. pyrestkit/ai/provider.py +58 -0
  13. pyrestkit/ai/providers/__init__.py +21 -0
  14. pyrestkit/ai/providers/anthropic.py +85 -0
  15. pyrestkit/ai/providers/azure_openai.py +84 -0
  16. pyrestkit/ai/providers/base.py +39 -0
  17. pyrestkit/ai/providers/bedrock.py +70 -0
  18. pyrestkit/ai/providers/cohere.py +82 -0
  19. pyrestkit/ai/providers/gemini.py +113 -0
  20. pyrestkit/ai/providers/groq.py +81 -0
  21. pyrestkit/ai/providers/mistral.py +88 -0
  22. pyrestkit/ai/providers/ollama.py +82 -0
  23. pyrestkit/ai/providers/openai.py +124 -0
  24. pyrestkit/ai/utils/__init__.py +0 -0
  25. pyrestkit/ai/utils/prompt_loader.py +52 -0
  26. pyrestkit/assertions/__init__.py +7 -0
  27. pyrestkit/assertions/assertion_exception.py +4 -0
  28. pyrestkit/assertions/response_assertions.py +181 -0
  29. pyrestkit/auth/__init__.py +11 -0
  30. pyrestkit/auth/auth_strategy.py +14 -0
  31. pyrestkit/auth/authentication_manager.py +35 -0
  32. pyrestkit/auth/strategies/__init__.py +5 -0
  33. pyrestkit/auth/strategies/api_key_auth.py +18 -0
  34. pyrestkit/auth/strategies/basic_auth.py +24 -0
  35. pyrestkit/auth/strategies/bearer_auth.py +17 -0
  36. pyrestkit/auth/token_cache.py +44 -0
  37. pyrestkit/auth/token_manager.py +32 -0
  38. pyrestkit/auth/token_provider.py +12 -0
  39. pyrestkit/auth/token_response.py +13 -0
  40. pyrestkit/builder/__init__.py +5 -0
  41. pyrestkit/builder/fluent_request_builder.py +167 -0
  42. pyrestkit/clients/__init__.py +7 -0
  43. pyrestkit/clients/base_client.py +68 -0
  44. pyrestkit/clients/user_client.py +66 -0
  45. pyrestkit/config/__init__.py +5 -0
  46. pyrestkit/config/config.py +97 -0
  47. pyrestkit/constants/__init__.py +0 -0
  48. pyrestkit/constants/content_types.py +0 -0
  49. pyrestkit/constants/headers.py +0 -0
  50. pyrestkit/constants/status_codes.py +0 -0
  51. pyrestkit/core/__init__.py +13 -0
  52. pyrestkit/core/api_client.py +129 -0
  53. pyrestkit/core/logger.py +41 -0
  54. pyrestkit/core/request_builder.py +45 -0
  55. pyrestkit/core/request_executor.py +64 -0
  56. pyrestkit/core/request_logger.py +0 -0
  57. pyrestkit/core/response_logger.py +0 -0
  58. pyrestkit/core/session_manager.py +19 -0
  59. pyrestkit/database/__init__.py +0 -0
  60. pyrestkit/endpoints/__init__.py +5 -0
  61. pyrestkit/endpoints/base_endpoints.py +32 -0
  62. pyrestkit/endpoints/order_endpoints.py +9 -0
  63. pyrestkit/endpoints/payment_endpoints.py +5 -0
  64. pyrestkit/endpoints/user_endpoints.py +48 -0
  65. pyrestkit/exceptions/__init__.py +21 -0
  66. pyrestkit/exceptions/api_exception.py +8 -0
  67. pyrestkit/exceptions/authentication_exception.py +10 -0
  68. pyrestkit/exceptions/configuration_exception.py +10 -0
  69. pyrestkit/exceptions/exception_mapper.py +32 -0
  70. pyrestkit/exceptions/network_exception.py +10 -0
  71. pyrestkit/exceptions/response_exception.py +10 -0
  72. pyrestkit/exceptions/serialization_exception.py +10 -0
  73. pyrestkit/exceptions/validation_exception.py +10 -0
  74. pyrestkit/factories/__init__.py +5 -0
  75. pyrestkit/factories/base_factory.py +25 -0
  76. pyrestkit/factories/user_factory.py +37 -0
  77. pyrestkit/hooks/__init__.py +5 -0
  78. pyrestkit/hooks/hook.py +27 -0
  79. pyrestkit/hooks/hook_manager.py +39 -0
  80. pyrestkit/hooks/request_hook.py +18 -0
  81. pyrestkit/hooks/response_hook.py +17 -0
  82. pyrestkit/hooks/timing_hook.py +32 -0
  83. pyrestkit/models/__init__.py +8 -0
  84. pyrestkit/models/base_response.py +11 -0
  85. pyrestkit/models/request/__init__.py +7 -0
  86. pyrestkit/models/request/create_user_request.py +11 -0
  87. pyrestkit/models/request/update_user_request.py +10 -0
  88. pyrestkit/models/response/__init__.py +5 -0
  89. pyrestkit/models/response/create_user_response.py +26 -0
  90. pyrestkit/models/response/get_user_response.py +28 -0
  91. pyrestkit/models/response/user_response.py +12 -0
  92. pyrestkit/pipeline/__init__.py +5 -0
  93. pyrestkit/pipeline/middleware.py +18 -0
  94. pyrestkit/pipeline/middleware_chain.py +11 -0
  95. pyrestkit/pipeline/pipeline.py +27 -0
  96. pyrestkit/pipeline/request_context.py +26 -0
  97. pyrestkit/response/__init__.py +8 -0
  98. pyrestkit/response/framework_response.py +271 -0
  99. pyrestkit/response/response_body.py +124 -0
  100. pyrestkit/retry/__init__.py +5 -0
  101. pyrestkit/retry/backoff.py +32 -0
  102. pyrestkit/retry/retry_handler.py +52 -0
  103. pyrestkit/retry/retry_policy.py +33 -0
  104. pyrestkit/serializers/__init__.py +0 -0
  105. pyrestkit/serializers/response_mapper.py +25 -0
  106. pyrestkit/types/__init__.py +0 -0
  107. pyrestkit/types/model_protocol.py +17 -0
  108. pyrestkit/utils/__init__.py +0 -0
  109. pyrestkit/validators/__init__.py +7 -0
  110. pyrestkit/validators/response_validator.py +57 -0
  111. pyrestkit/validators/schema_validator.py +33 -0
  112. pyrestkit-1.2.0.dist-info/METADATA +741 -0
  113. pyrestkit-1.2.0.dist-info/RECORD +115 -0
  114. pyrestkit-1.2.0.dist-info/WHEEL +5 -0
  115. pyrestkit-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pyrestkit.ai.config import AIConfig
6
+ from pyrestkit.ai.exceptions import AIConfigurationError, AIProviderError
7
+ from pyrestkit.ai.providers.base import BaseAIProvider
8
+
9
+
10
+ class AnthropicProvider(BaseAIProvider):
11
+ """
12
+ Anthropic provider using the Messages API.
13
+ """
14
+
15
+ def __init__(self, session: Any | None = None) -> None:
16
+ super().__init__(session=session)
17
+
18
+ def complete(
19
+ self,
20
+ prompt: str,
21
+ *,
22
+ config: AIConfig,
23
+ system_prompt: str | None = None,
24
+ ) -> str:
25
+ if not config.api_key:
26
+ raise AIConfigurationError("Anthropic API key is required.")
27
+
28
+ payload: dict[str, Any] = {
29
+ "model": config.model,
30
+ "max_tokens": config.max_tokens or 1024,
31
+ "messages": [{"role": "user", "content": prompt}],
32
+ }
33
+
34
+ if system_prompt:
35
+ payload["system"] = system_prompt
36
+
37
+ response = self._session.post(
38
+ _messages_url(config.base_url),
39
+ json=payload,
40
+ headers=_headers(config),
41
+ timeout=config.timeout,
42
+ )
43
+
44
+ if response.status_code >= 400:
45
+ raise AIProviderError(
46
+ f"Anthropic request failed with status {response.status_code}: "
47
+ f"{response.text}"
48
+ )
49
+
50
+ try:
51
+ data = response.json()
52
+ except ValueError as exc:
53
+ raise AIProviderError("Anthropic response was not valid JSON.") from exc
54
+
55
+ if not isinstance(data, dict):
56
+ raise AIProviderError("Anthropic response was not a JSON object.")
57
+
58
+ content = data.get("content")
59
+ if isinstance(content, list):
60
+ parts: list[str] = []
61
+ for item in content:
62
+ if isinstance(item, dict):
63
+ text = item.get("text")
64
+ if isinstance(text, str):
65
+ parts.append(text)
66
+ if parts:
67
+ return "\n".join(parts)
68
+
69
+ raise AIProviderError("Anthropic response did not include text content.")
70
+
71
+
72
+ def _messages_url(base_url: str | None) -> str:
73
+ base = (base_url or "https://api.anthropic.com/v1").rstrip("/")
74
+ return f"{base}/messages"
75
+
76
+
77
+ def _headers(config: AIConfig) -> dict[str, str]:
78
+ headers = BaseAIProvider.json_headers(config)
79
+
80
+ headers["anthropic-version"] = "2023-06-01"
81
+
82
+ if config.api_key is not None:
83
+ headers["x-api-key"] = config.api_key
84
+
85
+ return headers
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pyrestkit.ai.config import AIConfig
6
+ from pyrestkit.ai.exceptions import AIConfigurationError, AIProviderError
7
+ from pyrestkit.ai.providers.base import BaseAIProvider
8
+
9
+
10
+ class AzureOpenAIProvider(BaseAIProvider):
11
+ """
12
+ Azure OpenAI provider using the chat/completions endpoint.
13
+ """
14
+
15
+ def __init__(self, session: Any | None = None) -> None:
16
+ super().__init__(session=session)
17
+
18
+ def complete(
19
+ self,
20
+ prompt: str,
21
+ *,
22
+ config: AIConfig,
23
+ system_prompt: str | None = None,
24
+ ) -> str:
25
+ if not config.api_key:
26
+ raise AIConfigurationError("Azure OpenAI API key is required.")
27
+
28
+ payload: dict[str, Any] = {
29
+ "model": config.model,
30
+ "messages": [
31
+ {
32
+ "role": "system",
33
+ "content": system_prompt or "You are a helpful assistant.",
34
+ },
35
+ {"role": "user", "content": prompt},
36
+ ],
37
+ }
38
+
39
+ response = self._session.post(
40
+ _chat_url(config.base_url),
41
+ json=payload,
42
+ headers=_headers(config),
43
+ timeout=config.timeout,
44
+ )
45
+
46
+ if response.status_code >= 400:
47
+ raise AIProviderError(
48
+ f"Azure OpenAI request failed with status {response.status_code}: "
49
+ f"{response.text}"
50
+ )
51
+
52
+ try:
53
+ data = response.json()
54
+ except ValueError as exc:
55
+ raise AIProviderError("Azure OpenAI response was not valid JSON.") from exc
56
+
57
+ if not isinstance(data, dict):
58
+ raise AIProviderError("Azure OpenAI response was not a JSON object.")
59
+
60
+ choices = data.get("choices")
61
+ if isinstance(choices, list) and choices:
62
+ first = choices[0]
63
+ if isinstance(first, dict):
64
+ message = first.get("message")
65
+ if isinstance(message, dict):
66
+ content = message.get("content")
67
+ if isinstance(content, str):
68
+ return content
69
+
70
+ raise AIProviderError("Azure OpenAI response did not include text content.")
71
+
72
+
73
+ def _chat_url(base_url: str | None) -> str:
74
+ base = (base_url or "https://example.openai.azure.com").rstrip("/")
75
+ return f"{base}/openai/deployments?api-version=2024-02-01"
76
+
77
+
78
+ def _headers(config: AIConfig) -> dict[str, str]:
79
+ headers = BaseAIProvider.json_headers(config)
80
+
81
+ if config.api_key is not None:
82
+ headers["api-key"] = config.api_key
83
+
84
+ return headers
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+ import requests
7
+
8
+ from pyrestkit.ai.config import AIConfig
9
+
10
+
11
+ class BaseAIProvider(ABC):
12
+ """
13
+ Base class for all AI providers.
14
+ """
15
+
16
+ def __init__(self, session: Any | None = None) -> None:
17
+ self._session = session or requests.Session()
18
+
19
+ @staticmethod
20
+ def json_headers(config: AIConfig) -> dict[str, str]:
21
+ """
22
+ Build common JSON headers.
23
+ """
24
+ headers: dict[str, str] = {
25
+ "Content-Type": "application/json",
26
+ }
27
+
28
+ headers.update(config.headers)
29
+
30
+ return headers
31
+
32
+ @abstractmethod
33
+ def complete(
34
+ self,
35
+ prompt: str,
36
+ *,
37
+ config: AIConfig,
38
+ system_prompt: str | None = None,
39
+ ) -> str: ...
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pyrestkit.ai.config import AIConfig
6
+ from pyrestkit.ai.exceptions import AIConfigurationError, AIProviderError
7
+ from pyrestkit.ai.providers.base import BaseAIProvider
8
+
9
+
10
+ class BedrockProvider(BaseAIProvider):
11
+ """
12
+ AWS Bedrock provider using a simple text-generation style payload.
13
+ """
14
+
15
+ def __init__(self, session: Any | None = None) -> None:
16
+ super().__init__(session=session)
17
+
18
+ def complete(
19
+ self,
20
+ prompt: str,
21
+ *,
22
+ config: AIConfig,
23
+ system_prompt: str | None = None,
24
+ ) -> str:
25
+ if not config.api_key:
26
+ raise AIConfigurationError("Bedrock API key is required.")
27
+
28
+ payload: dict[str, Any] = {
29
+ "model": config.model,
30
+ "inputText": prompt,
31
+ }
32
+
33
+ if system_prompt:
34
+ payload["systemPrompt"] = system_prompt
35
+
36
+ response = self._session.post(
37
+ _invoke_url(config.base_url),
38
+ json=payload,
39
+ headers=_headers(config),
40
+ timeout=config.timeout,
41
+ )
42
+
43
+ if response.status_code >= 400:
44
+ raise AIProviderError(
45
+ f"Bedrock request failed with status {response.status_code}: "
46
+ f"{response.text}"
47
+ )
48
+
49
+ try:
50
+ data = response.json()
51
+ except ValueError as exc:
52
+ raise AIProviderError("Bedrock response was not valid JSON.") from exc
53
+
54
+ if not isinstance(data, dict):
55
+ raise AIProviderError("Bedrock response was not a JSON object.")
56
+
57
+ output = data.get("outputText")
58
+ if isinstance(output, str) and output.strip():
59
+ return output
60
+
61
+ raise AIProviderError("Bedrock response did not include text content.")
62
+
63
+
64
+ def _invoke_url(base_url: str | None) -> str:
65
+ base = (base_url or "https://bedrock-runtime.us-east-1.amazonaws.com").rstrip("/")
66
+ return f"{base}/model:invoke"
67
+
68
+
69
+ def _headers(config: AIConfig) -> dict[str, str]:
70
+ return BaseAIProvider.json_headers(config)
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pyrestkit.ai.config import AIConfig
6
+ from pyrestkit.ai.exceptions import AIConfigurationError, AIProviderError
7
+ from pyrestkit.ai.providers.base import BaseAIProvider
8
+
9
+
10
+ class CohereProvider(BaseAIProvider):
11
+ """
12
+ Cohere provider using the chat endpoint.
13
+ """
14
+
15
+ def __init__(self, session: Any | None = None) -> None:
16
+ super().__init__(session=session)
17
+
18
+ def complete(
19
+ self,
20
+ prompt: str,
21
+ *,
22
+ config: AIConfig,
23
+ system_prompt: str | None = None,
24
+ ) -> str:
25
+ if not config.api_key:
26
+ raise AIConfigurationError("Cohere API key is required.")
27
+
28
+ payload: dict[str, Any] = {
29
+ "model": config.model,
30
+ "message": prompt,
31
+ }
32
+
33
+ if system_prompt:
34
+ payload["chat_history"] = [{"role": "SYSTEM", "message": system_prompt}]
35
+
36
+ response = self._session.post(
37
+ _chat_url(config.base_url),
38
+ json=payload,
39
+ headers=_headers(config),
40
+ timeout=config.timeout,
41
+ )
42
+
43
+ if response.status_code >= 400:
44
+ raise AIProviderError(
45
+ f"Cohere request failed with status {response.status_code}: "
46
+ f"{response.text}"
47
+ )
48
+
49
+ try:
50
+ data = response.json()
51
+ except ValueError as exc:
52
+ raise AIProviderError("Cohere response was not valid JSON.") from exc
53
+
54
+ if not isinstance(data, dict):
55
+ raise AIProviderError("Cohere response was not a JSON object.")
56
+
57
+ texts = data.get("text")
58
+ if isinstance(texts, str) and texts.strip():
59
+ return texts
60
+
61
+ raise AIProviderError("Cohere response did not include text content.")
62
+
63
+
64
+ def _chat_url(base_url: str | None) -> str:
65
+ base = (base_url or "https://api.cohere.com/v1").rstrip("/")
66
+ return f"{base}/chat"
67
+
68
+
69
+ def _headers(config: AIConfig) -> dict[str, str]:
70
+ headers: dict[str, str] = {
71
+ "Content-Type": "application/json",
72
+ }
73
+
74
+ if config.api_key is not None:
75
+ headers["Authorization"] = f"Bearer {config.api_key}"
76
+
77
+ if config.organization is not None:
78
+ headers["OpenAI-Organization"] = config.organization
79
+
80
+ headers.update(config.headers)
81
+
82
+ return headers
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pyrestkit.ai.config import AIConfig
6
+ from pyrestkit.ai.exceptions import AIConfigurationError, AIProviderError
7
+ from pyrestkit.ai.providers.base import BaseAIProvider
8
+
9
+
10
+ class GeminiProvider(BaseAIProvider):
11
+ """
12
+ Google Gemini provider using the generateContent endpoint.
13
+ """
14
+
15
+ def __init__(self, session: Any | None = None) -> None:
16
+ super().__init__(session=session)
17
+
18
+ def complete(
19
+ self,
20
+ prompt: str,
21
+ *,
22
+ config: AIConfig,
23
+ system_prompt: str | None = None,
24
+ ) -> str:
25
+ if not config.api_key:
26
+ raise AIConfigurationError("Gemini API key is required.")
27
+
28
+ content: dict[str, Any] = {
29
+ "parts": [
30
+ {
31
+ "text": prompt,
32
+ }
33
+ ]
34
+ }
35
+
36
+ payload: dict[str, Any] = {
37
+ "contents": [content],
38
+ }
39
+
40
+ if system_prompt:
41
+ payload["system_instruction"] = {
42
+ "parts": [
43
+ {
44
+ "text": system_prompt,
45
+ }
46
+ ]
47
+ }
48
+
49
+ response = self._session.post(
50
+ _generate_url(config.base_url),
51
+ json=payload,
52
+ headers=_headers(config),
53
+ timeout=config.timeout,
54
+ )
55
+
56
+ if response.status_code >= 400:
57
+ raise AIProviderError(
58
+ f"Gemini request failed with status "
59
+ f"{response.status_code}: {response.text}"
60
+ )
61
+
62
+ try:
63
+ data = response.json()
64
+ except ValueError as exc:
65
+ raise AIProviderError("Gemini response was not valid JSON.") from exc
66
+
67
+ if not isinstance(data, dict):
68
+ raise AIProviderError("Gemini response was not a JSON object.")
69
+
70
+ candidates = data.get("candidates")
71
+
72
+ if isinstance(candidates, list):
73
+ for candidate in candidates:
74
+ if not isinstance(candidate, dict):
75
+ continue
76
+
77
+ candidate_content = candidate.get("content")
78
+ if not isinstance(candidate_content, dict):
79
+ continue
80
+
81
+ parts = candidate_content.get("parts")
82
+ if not isinstance(parts, list):
83
+ continue
84
+
85
+ texts: list[str] = []
86
+
87
+ for part in parts:
88
+ if not isinstance(part, dict):
89
+ continue
90
+
91
+ text = part.get("text")
92
+ if isinstance(text, str):
93
+ texts.append(text)
94
+
95
+ if texts:
96
+ return "\n".join(texts)
97
+
98
+ raise AIProviderError("Gemini response did not include text content.")
99
+
100
+
101
+ def _generate_url(base_url: str | None) -> str:
102
+ base = (base_url or "https://generativelanguage.googleapis.com/v1beta").rstrip("/")
103
+
104
+ return f"{base}/models/gemini-2.0-flash:generateContent"
105
+
106
+
107
+ def _headers(config: AIConfig) -> dict[str, str]:
108
+ headers = BaseAIProvider.json_headers(config)
109
+
110
+ if config.api_key is not None:
111
+ headers["x-goog-api-key"] = config.api_key
112
+
113
+ return headers
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pyrestkit.ai.config import AIConfig
6
+ from pyrestkit.ai.exceptions import AIConfigurationError, AIProviderError
7
+ from pyrestkit.ai.providers.base import BaseAIProvider
8
+
9
+
10
+ class GroqProvider(BaseAIProvider):
11
+ """
12
+ Groq provider using the chat/completions endpoint.
13
+ """
14
+
15
+ def __init__(self, session: Any | None = None) -> None:
16
+ super().__init__(session=session)
17
+
18
+ def complete(
19
+ self,
20
+ prompt: str,
21
+ *,
22
+ config: AIConfig,
23
+ system_prompt: str | None = None,
24
+ ) -> str:
25
+ if not config.api_key:
26
+ raise AIConfigurationError("Groq API key is required.")
27
+
28
+ payload: dict[str, Any] = {
29
+ "model": config.model,
30
+ "messages": [{"role": "user", "content": prompt}],
31
+ }
32
+
33
+ if system_prompt:
34
+ payload["messages"].insert(0, {"role": "system", "content": system_prompt})
35
+
36
+ response = self._session.post(
37
+ _chat_url(config.base_url),
38
+ json=payload,
39
+ headers=_headers(config),
40
+ timeout=config.timeout,
41
+ )
42
+
43
+ if response.status_code >= 400:
44
+ raise AIProviderError(
45
+ f"Groq request failed with status {response.status_code}: "
46
+ f"{response.text}"
47
+ )
48
+
49
+ try:
50
+ data = response.json()
51
+ except ValueError as exc:
52
+ raise AIProviderError("Groq response was not valid JSON.") from exc
53
+
54
+ if not isinstance(data, dict):
55
+ raise AIProviderError("Groq response was not a JSON object.")
56
+
57
+ choices = data.get("choices")
58
+ if isinstance(choices, list) and choices:
59
+ first = choices[0]
60
+ if isinstance(first, dict):
61
+ message = first.get("message")
62
+ if isinstance(message, dict):
63
+ content = message.get("content")
64
+ if isinstance(content, str):
65
+ return content
66
+
67
+ raise AIProviderError("Groq response did not include text content.")
68
+
69
+
70
+ def _chat_url(base_url: str | None) -> str:
71
+ base = (base_url or "https://api.groq.com/openai/v1").rstrip("/")
72
+ return f"{base}/chat/completions"
73
+
74
+
75
+ def _headers(config: AIConfig) -> dict[str, str]:
76
+ headers = BaseAIProvider.json_headers(config)
77
+
78
+ if config.api_key is not None:
79
+ headers["Authorization"] = f"Bearer {config.api_key}"
80
+
81
+ return headers
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pyrestkit.ai.config import AIConfig
6
+ from pyrestkit.ai.exceptions import AIConfigurationError, AIProviderError
7
+ from pyrestkit.ai.providers.base import BaseAIProvider
8
+
9
+
10
+ class MistralProvider(BaseAIProvider):
11
+ """
12
+ Mistral provider using the chat/completions endpoint.
13
+ """
14
+
15
+ def __init__(self, session: Any | None = None) -> None:
16
+ super().__init__(session=session)
17
+
18
+ def complete(
19
+ self,
20
+ prompt: str,
21
+ *,
22
+ config: AIConfig,
23
+ system_prompt: str | None = None,
24
+ ) -> str:
25
+ if not config.api_key:
26
+ raise AIConfigurationError("Mistral API key is required.")
27
+
28
+ payload: dict[str, Any] = {
29
+ "model": config.model,
30
+ "messages": [{"role": "user", "content": prompt}],
31
+ }
32
+
33
+ if system_prompt:
34
+ payload["messages"].insert(0, {"role": "system", "content": system_prompt})
35
+
36
+ response = self._session.post(
37
+ _chat_url(config.base_url),
38
+ json=payload,
39
+ headers=_headers(config),
40
+ timeout=config.timeout,
41
+ )
42
+
43
+ if response.status_code >= 400:
44
+ raise AIProviderError(
45
+ f"Mistral request failed with status {response.status_code}: "
46
+ f"{response.text}"
47
+ )
48
+
49
+ try:
50
+ data = response.json()
51
+ except ValueError as exc:
52
+ raise AIProviderError("Mistral response was not valid JSON.") from exc
53
+
54
+ if not isinstance(data, dict):
55
+ raise AIProviderError("Mistral response was not a JSON object.")
56
+
57
+ choices = data.get("choices")
58
+ if isinstance(choices, list) and choices:
59
+ first = choices[0]
60
+ if isinstance(first, dict):
61
+ message = first.get("message")
62
+ if isinstance(message, dict):
63
+ content = message.get("content")
64
+ if isinstance(content, str):
65
+ return content
66
+
67
+ raise AIProviderError("Mistral response did not include text content.")
68
+
69
+
70
+ def _chat_url(base_url: str | None) -> str:
71
+ base = (base_url or "https://api.mistral.ai/v1").rstrip("/")
72
+ return f"{base}/chat/completions"
73
+
74
+
75
+ def _headers(config: AIConfig) -> dict[str, str]:
76
+ headers: dict[str, str] = {
77
+ "Content-Type": "application/json",
78
+ }
79
+
80
+ if config.api_key is not None:
81
+ headers["Authorization"] = f"Bearer {config.api_key}"
82
+
83
+ if config.organization is not None:
84
+ headers["OpenAI-Organization"] = config.organization
85
+
86
+ headers.update(config.headers)
87
+
88
+ return headers