deepeval 3.5.2__py3-none-any.whl → 3.5.3__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.
- deepeval/_version.py +1 -1
- deepeval/config/settings.py +94 -2
- deepeval/config/utils.py +54 -1
- deepeval/constants.py +27 -0
- deepeval/metrics/pii_leakage/pii_leakage.py +1 -1
- deepeval/models/embedding_models/azure_embedding_model.py +40 -9
- deepeval/models/embedding_models/local_embedding_model.py +52 -9
- deepeval/models/embedding_models/ollama_embedding_model.py +25 -7
- deepeval/models/embedding_models/openai_embedding_model.py +47 -5
- deepeval/models/llms/amazon_bedrock_model.py +31 -4
- deepeval/models/llms/anthropic_model.py +39 -13
- deepeval/models/llms/azure_model.py +37 -38
- deepeval/models/llms/deepseek_model.py +36 -7
- deepeval/models/llms/gemini_model.py +10 -0
- deepeval/models/llms/grok_model.py +50 -3
- deepeval/models/llms/kimi_model.py +37 -7
- deepeval/models/llms/local_model.py +38 -12
- deepeval/models/llms/ollama_model.py +15 -3
- deepeval/models/llms/openai_model.py +37 -44
- deepeval/models/mlllms/gemini_model.py +21 -3
- deepeval/models/mlllms/ollama_model.py +38 -13
- deepeval/models/mlllms/openai_model.py +18 -42
- deepeval/models/retry_policy.py +548 -64
- deepeval/tracing/tracing.py +87 -0
- {deepeval-3.5.2.dist-info → deepeval-3.5.3.dist-info}/METADATA +1 -1
- {deepeval-3.5.2.dist-info → deepeval-3.5.3.dist-info}/RECORD +29 -29
- {deepeval-3.5.2.dist-info → deepeval-3.5.3.dist-info}/LICENSE.md +0 -0
- {deepeval-3.5.2.dist-info → deepeval-3.5.3.dist-info}/WHEEL +0 -0
- {deepeval-3.5.2.dist-info → deepeval-3.5.3.dist-info}/entry_points.txt +0 -0
deepeval/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__: str = "3.5.
|
|
1
|
+
__version__: str = "3.5.3"
|
deepeval/config/settings.py
CHANGED
|
@@ -9,6 +9,7 @@ Central config for DeepEval.
|
|
|
9
9
|
type coercion.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import logging
|
|
12
13
|
import os
|
|
13
14
|
import re
|
|
14
15
|
|
|
@@ -16,11 +17,17 @@ from dotenv import dotenv_values
|
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from pydantic import AnyUrl, SecretStr, field_validator, confloat
|
|
18
19
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
19
|
-
from typing import Any, Dict, Optional, NamedTuple
|
|
20
|
+
from typing import Any, Dict, List, Optional, NamedTuple
|
|
20
21
|
|
|
21
|
-
from deepeval.config.utils import
|
|
22
|
+
from deepeval.config.utils import (
|
|
23
|
+
parse_bool,
|
|
24
|
+
coerce_to_list,
|
|
25
|
+
dedupe_preserve_order,
|
|
26
|
+
)
|
|
27
|
+
from deepeval.constants import SUPPORTED_PROVIDER_SLUGS, slugify
|
|
22
28
|
|
|
23
29
|
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
24
31
|
_SAVE_RE = re.compile(r"^(?P<scheme>dotenv)(?::(?P<path>.+))?$")
|
|
25
32
|
|
|
26
33
|
|
|
@@ -264,6 +271,13 @@ class Settings(BaseSettings):
|
|
|
264
271
|
LOCAL_EMBEDDING_MODEL_NAME: Optional[str] = None
|
|
265
272
|
LOCAL_EMBEDDING_BASE_URL: Optional[AnyUrl] = None
|
|
266
273
|
|
|
274
|
+
#
|
|
275
|
+
# Retry Policy
|
|
276
|
+
#
|
|
277
|
+
DEEPEVAL_SDK_RETRY_PROVIDERS: Optional[List[str]] = None
|
|
278
|
+
DEEPEVAL_RETRY_BEFORE_LOG_LEVEL: Optional[int] = None # default -> INFO
|
|
279
|
+
DEEPEVAL_RETRY_AFTER_LOG_LEVEL: Optional[int] = None # default -> ERROR
|
|
280
|
+
|
|
267
281
|
#
|
|
268
282
|
# Telemetry and Debug
|
|
269
283
|
#
|
|
@@ -283,6 +297,12 @@ class Settings(BaseSettings):
|
|
|
283
297
|
CONFIDENT_SAMPLE_RATE: Optional[float] = 1.0
|
|
284
298
|
OTEL_EXPORTER_OTLP_ENDPOINT: Optional[AnyUrl] = None
|
|
285
299
|
|
|
300
|
+
#
|
|
301
|
+
# Network
|
|
302
|
+
#
|
|
303
|
+
MEDIA_IMAGE_CONNECT_TIMEOUT_SECONDS: float = 3.05
|
|
304
|
+
MEDIA_IMAGE_READ_TIMEOUT_SECONDS: float = 10.0
|
|
305
|
+
|
|
286
306
|
##############
|
|
287
307
|
# Validators #
|
|
288
308
|
##############
|
|
@@ -401,6 +421,78 @@ class Settings(BaseSettings):
|
|
|
401
421
|
return None
|
|
402
422
|
return s.upper()
|
|
403
423
|
|
|
424
|
+
@field_validator("DEEPEVAL_SDK_RETRY_PROVIDERS", mode="before")
|
|
425
|
+
@classmethod
|
|
426
|
+
def _coerce_to_list(cls, v):
|
|
427
|
+
# works with JSON list, comma/space/semicolon separated, or real lists
|
|
428
|
+
return coerce_to_list(v, lower=True)
|
|
429
|
+
|
|
430
|
+
@field_validator("DEEPEVAL_SDK_RETRY_PROVIDERS", mode="after")
|
|
431
|
+
@classmethod
|
|
432
|
+
def _validate_sdk_provider_list(cls, v):
|
|
433
|
+
if v is None:
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
normalized: list[str] = []
|
|
437
|
+
star = False
|
|
438
|
+
|
|
439
|
+
for item in v:
|
|
440
|
+
s = str(item).strip()
|
|
441
|
+
if not s:
|
|
442
|
+
continue
|
|
443
|
+
if s == "*":
|
|
444
|
+
star = True
|
|
445
|
+
continue
|
|
446
|
+
s = slugify(s)
|
|
447
|
+
if s in SUPPORTED_PROVIDER_SLUGS:
|
|
448
|
+
normalized.append(s)
|
|
449
|
+
else:
|
|
450
|
+
if cls.DEEPEVAL_VERBOSE_MODE:
|
|
451
|
+
logger.warning("Unknown provider slug %r dropped", item)
|
|
452
|
+
|
|
453
|
+
if star:
|
|
454
|
+
return ["*"]
|
|
455
|
+
|
|
456
|
+
# It is important to dedup after normalization to catch variants
|
|
457
|
+
normalized = dedupe_preserve_order(normalized)
|
|
458
|
+
return normalized or None
|
|
459
|
+
|
|
460
|
+
@field_validator(
|
|
461
|
+
"DEEPEVAL_RETRY_BEFORE_LOG_LEVEL",
|
|
462
|
+
"DEEPEVAL_RETRY_AFTER_LOG_LEVEL",
|
|
463
|
+
mode="before",
|
|
464
|
+
)
|
|
465
|
+
@classmethod
|
|
466
|
+
def _coerce_log_level(cls, v):
|
|
467
|
+
if v is None:
|
|
468
|
+
return None
|
|
469
|
+
if isinstance(v, (int, float)):
|
|
470
|
+
return int(v)
|
|
471
|
+
|
|
472
|
+
s = str(v).strip().upper()
|
|
473
|
+
if not s:
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
import logging
|
|
477
|
+
|
|
478
|
+
# Accept standard names or numeric strings
|
|
479
|
+
name_to_level = {
|
|
480
|
+
"CRITICAL": logging.CRITICAL,
|
|
481
|
+
"ERROR": logging.ERROR,
|
|
482
|
+
"WARNING": logging.WARNING,
|
|
483
|
+
"INFO": logging.INFO,
|
|
484
|
+
"DEBUG": logging.DEBUG,
|
|
485
|
+
"NOTSET": logging.NOTSET,
|
|
486
|
+
}
|
|
487
|
+
if s.isdigit() or (s.startswith("-") and s[1:].isdigit()):
|
|
488
|
+
return int(s)
|
|
489
|
+
if s in name_to_level:
|
|
490
|
+
return name_to_level[s]
|
|
491
|
+
raise ValueError(
|
|
492
|
+
"Retry log level must be one of DEBUG, INFO, WARNING, ERROR, "
|
|
493
|
+
"CRITICAL, NOTSET, or a numeric logging level."
|
|
494
|
+
)
|
|
495
|
+
|
|
404
496
|
#######################
|
|
405
497
|
# Persistence support #
|
|
406
498
|
#######################
|
deepeval/config/utils.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
|
-
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from typing import Any, Iterable, List, Optional
|
|
6
|
+
|
|
3
7
|
|
|
4
8
|
_TRUTHY = frozenset({"1", "true", "t", "yes", "y", "on", "enable", "enabled"})
|
|
5
9
|
_FALSY = frozenset({"0", "false", "f", "no", "n", "off", "disable", "disabled"})
|
|
10
|
+
_LIST_SEP_RE = re.compile(r"[,\s;]+")
|
|
6
11
|
|
|
7
12
|
|
|
8
13
|
def parse_bool(value: Any, default: bool = False) -> bool:
|
|
@@ -84,3 +89,51 @@ def set_env_bool(key: str, value: Optional[bool] = False) -> None:
|
|
|
84
89
|
- Use `get_env_bool` to read back and parse the value safely.
|
|
85
90
|
"""
|
|
86
91
|
os.environ[key] = bool_to_env_str(bool(value))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def coerce_to_list(
|
|
95
|
+
v,
|
|
96
|
+
*,
|
|
97
|
+
lower: bool = False,
|
|
98
|
+
allow_json: bool = True,
|
|
99
|
+
sep_re: re.Pattern = _LIST_SEP_RE,
|
|
100
|
+
) -> Optional[List[str]]:
|
|
101
|
+
"""
|
|
102
|
+
Coerce None / str / list / tuple / set into a clean List[str].
|
|
103
|
+
- Accepts JSON arrays ("[...]"") or delimited strings (comma/space/semicolon).
|
|
104
|
+
- Strips whitespace, drops empties, optionally lowercases.
|
|
105
|
+
"""
|
|
106
|
+
if v is None:
|
|
107
|
+
return None
|
|
108
|
+
if isinstance(v, (list, tuple, set)):
|
|
109
|
+
items = list(v)
|
|
110
|
+
else:
|
|
111
|
+
s = str(v).strip()
|
|
112
|
+
if not s:
|
|
113
|
+
return None
|
|
114
|
+
if allow_json and s.startswith("[") and s.endswith("]"):
|
|
115
|
+
try:
|
|
116
|
+
parsed = json.loads(s)
|
|
117
|
+
items = parsed if isinstance(parsed, list) else [s]
|
|
118
|
+
except Exception:
|
|
119
|
+
items = sep_re.split(s)
|
|
120
|
+
else:
|
|
121
|
+
items = sep_re.split(s)
|
|
122
|
+
|
|
123
|
+
out: List[str] = []
|
|
124
|
+
for item in items:
|
|
125
|
+
s = str(item).strip()
|
|
126
|
+
if not s:
|
|
127
|
+
continue
|
|
128
|
+
out.append(s.lower() if lower else s)
|
|
129
|
+
return out or None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def dedupe_preserve_order(items: Iterable[str]) -> List[str]:
|
|
133
|
+
seen = set()
|
|
134
|
+
out: List[str] = []
|
|
135
|
+
for x in items:
|
|
136
|
+
if x not in seen:
|
|
137
|
+
seen.add(x)
|
|
138
|
+
out.append(x)
|
|
139
|
+
return out
|
deepeval/constants.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
1
3
|
KEY_FILE: str = ".deepeval"
|
|
2
4
|
HIDDEN_DIR: str = ".deepeval"
|
|
3
5
|
PYTEST_RUN_TEST_NAME: str = "CONFIDENT_AI_RUN_TEST_NAME"
|
|
@@ -11,3 +13,28 @@ CONFIDENT_TRACE_ENVIRONMENT = "CONFIDENT_TRACE_ENVIRONMENT"
|
|
|
11
13
|
CONFIDENT_TRACING_ENABLED = "CONFIDENT_TRACING_ENABLED"
|
|
12
14
|
CONFIDENT_OPEN_BROWSER = "CONFIDENT_OPEN_BROWSER"
|
|
13
15
|
CONFIDENT_TEST_CASE_BATCH_SIZE = "CONFIDENT_TEST_CASE_BATCH_SIZE"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProviderSlug(str, Enum):
|
|
19
|
+
OPENAI = "openai"
|
|
20
|
+
AZURE = "azure"
|
|
21
|
+
ANTHROPIC = "anthropic"
|
|
22
|
+
BEDROCK = "bedrock"
|
|
23
|
+
DEEPSEEK = "deepseek"
|
|
24
|
+
GOOGLE = "google"
|
|
25
|
+
GROK = "grok"
|
|
26
|
+
KIMI = "kimi"
|
|
27
|
+
LITELLM = "litellm"
|
|
28
|
+
LOCAL = "local"
|
|
29
|
+
OLLAMA = "ollama"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def slugify(value: str | ProviderSlug) -> str:
|
|
33
|
+
return (
|
|
34
|
+
value.value
|
|
35
|
+
if isinstance(value, ProviderSlug)
|
|
36
|
+
else str(value).strip().lower()
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
SUPPORTED_PROVIDER_SLUGS = frozenset(s.value for s in ProviderSlug)
|
|
@@ -284,7 +284,7 @@ class PIILeakageMetric(BaseMetric):
|
|
|
284
284
|
no_privacy_count += 1
|
|
285
285
|
|
|
286
286
|
score = no_privacy_count / number_of_verdicts
|
|
287
|
-
return
|
|
287
|
+
return 0 if self.strict_mode and score < self.threshold else score
|
|
288
288
|
|
|
289
289
|
def is_successful(self) -> bool:
|
|
290
290
|
if self.error is not None:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import Dict, List
|
|
2
2
|
from openai import AzureOpenAI, AsyncAzureOpenAI
|
|
3
3
|
from deepeval.key_handler import (
|
|
4
4
|
EmbeddingKeyValues,
|
|
@@ -6,10 +6,18 @@ from deepeval.key_handler import (
|
|
|
6
6
|
KEY_FILE_HANDLER,
|
|
7
7
|
)
|
|
8
8
|
from deepeval.models import DeepEvalBaseEmbeddingModel
|
|
9
|
+
from deepeval.models.retry_policy import (
|
|
10
|
+
create_retry_decorator,
|
|
11
|
+
sdk_retries_for,
|
|
12
|
+
)
|
|
13
|
+
from deepeval.constants import ProviderSlug as PS
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
retry_azure = create_retry_decorator(PS.AZURE)
|
|
9
17
|
|
|
10
18
|
|
|
11
19
|
class AzureOpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
12
|
-
def __init__(self):
|
|
20
|
+
def __init__(self, **kwargs):
|
|
13
21
|
self.azure_openai_api_key = KEY_FILE_HANDLER.fetch_data(
|
|
14
22
|
ModelKeyValues.AZURE_OPENAI_API_KEY
|
|
15
23
|
)
|
|
@@ -23,7 +31,9 @@ class AzureOpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
23
31
|
ModelKeyValues.AZURE_OPENAI_ENDPOINT
|
|
24
32
|
)
|
|
25
33
|
self.model_name = self.azure_embedding_deployment
|
|
34
|
+
self.kwargs = kwargs
|
|
26
35
|
|
|
36
|
+
@retry_azure
|
|
27
37
|
def embed_text(self, text: str) -> List[float]:
|
|
28
38
|
client = self.load_model(async_mode=False)
|
|
29
39
|
response = client.embeddings.create(
|
|
@@ -32,6 +42,7 @@ class AzureOpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
32
42
|
)
|
|
33
43
|
return response.data[0].embedding
|
|
34
44
|
|
|
45
|
+
@retry_azure
|
|
35
46
|
def embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
36
47
|
client = self.load_model(async_mode=False)
|
|
37
48
|
response = client.embeddings.create(
|
|
@@ -40,6 +51,7 @@ class AzureOpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
40
51
|
)
|
|
41
52
|
return [item.embedding for item in response.data]
|
|
42
53
|
|
|
54
|
+
@retry_azure
|
|
43
55
|
async def a_embed_text(self, text: str) -> List[float]:
|
|
44
56
|
client = self.load_model(async_mode=True)
|
|
45
57
|
response = await client.embeddings.create(
|
|
@@ -48,6 +60,7 @@ class AzureOpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
48
60
|
)
|
|
49
61
|
return response.data[0].embedding
|
|
50
62
|
|
|
63
|
+
@retry_azure
|
|
51
64
|
async def a_embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
52
65
|
client = self.load_model(async_mode=True)
|
|
53
66
|
response = await client.embeddings.create(
|
|
@@ -61,15 +74,33 @@ class AzureOpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
61
74
|
|
|
62
75
|
def load_model(self, async_mode: bool = False):
|
|
63
76
|
if not async_mode:
|
|
64
|
-
return AzureOpenAI
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
return self._build_client(AzureOpenAI)
|
|
78
|
+
return self._build_client(AsyncAzureOpenAI)
|
|
79
|
+
|
|
80
|
+
def _client_kwargs(self) -> Dict:
|
|
81
|
+
"""
|
|
82
|
+
If Tenacity is managing retries, force OpenAI SDK retries off to avoid double retries.
|
|
83
|
+
If the user opts into SDK retries for 'azure' via DEEPEVAL_SDK_RETRY_PROVIDERS,
|
|
84
|
+
leave their retry settings as is.
|
|
85
|
+
"""
|
|
86
|
+
kwargs = dict(self.kwargs or {})
|
|
87
|
+
if not sdk_retries_for(PS.AZURE):
|
|
88
|
+
kwargs["max_retries"] = 0
|
|
89
|
+
return kwargs
|
|
90
|
+
|
|
91
|
+
def _build_client(self, cls):
|
|
92
|
+
kw = dict(
|
|
71
93
|
api_key=self.azure_openai_api_key,
|
|
72
94
|
api_version=self.openai_api_version,
|
|
73
95
|
azure_endpoint=self.azure_endpoint,
|
|
74
96
|
azure_deployment=self.azure_embedding_deployment,
|
|
97
|
+
**self._client_kwargs(),
|
|
75
98
|
)
|
|
99
|
+
try:
|
|
100
|
+
return cls(**kw)
|
|
101
|
+
except TypeError as e:
|
|
102
|
+
# older OpenAI SDKs may not accept max_retries, in that case remove and retry once
|
|
103
|
+
if "max_retries" in str(e):
|
|
104
|
+
kw.pop("max_retries", None)
|
|
105
|
+
return cls(**kw)
|
|
106
|
+
raise
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
from openai import OpenAI
|
|
2
|
-
from typing import List
|
|
1
|
+
from openai import OpenAI, AsyncOpenAI
|
|
2
|
+
from typing import Dict, List
|
|
3
3
|
|
|
4
4
|
from deepeval.key_handler import EmbeddingKeyValues, KEY_FILE_HANDLER
|
|
5
5
|
from deepeval.models import DeepEvalBaseEmbeddingModel
|
|
6
|
+
from deepeval.models.retry_policy import (
|
|
7
|
+
create_retry_decorator,
|
|
8
|
+
sdk_retries_for,
|
|
9
|
+
)
|
|
10
|
+
from deepeval.constants import ProviderSlug as PS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# consistent retry rules
|
|
14
|
+
retry_local = create_retry_decorator(PS.LOCAL)
|
|
6
15
|
|
|
7
16
|
|
|
8
17
|
class LocalEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
9
|
-
def __init__(self,
|
|
18
|
+
def __init__(self, **kwargs):
|
|
10
19
|
self.base_url = KEY_FILE_HANDLER.fetch_data(
|
|
11
20
|
EmbeddingKeyValues.LOCAL_EMBEDDING_BASE_URL
|
|
12
21
|
)
|
|
@@ -16,13 +25,10 @@ class LocalEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
16
25
|
self.api_key = KEY_FILE_HANDLER.fetch_data(
|
|
17
26
|
EmbeddingKeyValues.LOCAL_EMBEDDING_API_KEY
|
|
18
27
|
)
|
|
19
|
-
self.args = args
|
|
20
28
|
self.kwargs = kwargs
|
|
21
29
|
super().__init__(model_name)
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
return OpenAI(base_url=self.base_url, api_key=self.api_key)
|
|
25
|
-
|
|
31
|
+
@retry_local
|
|
26
32
|
def embed_text(self, text: str) -> List[float]:
|
|
27
33
|
embedding_model = self.load_model()
|
|
28
34
|
response = embedding_model.embeddings.create(
|
|
@@ -31,6 +37,7 @@ class LocalEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
31
37
|
)
|
|
32
38
|
return response.data[0].embedding
|
|
33
39
|
|
|
40
|
+
@retry_local
|
|
34
41
|
def embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
35
42
|
embedding_model = self.load_model()
|
|
36
43
|
response = embedding_model.embeddings.create(
|
|
@@ -39,21 +46,57 @@ class LocalEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
39
46
|
)
|
|
40
47
|
return [data.embedding for data in response.data]
|
|
41
48
|
|
|
49
|
+
@retry_local
|
|
42
50
|
async def a_embed_text(self, text: str) -> List[float]:
|
|
43
|
-
embedding_model = self.load_model()
|
|
51
|
+
embedding_model = self.load_model(async_mode=True)
|
|
44
52
|
response = await embedding_model.embeddings.create(
|
|
45
53
|
model=self.model_name,
|
|
46
54
|
input=[text],
|
|
47
55
|
)
|
|
48
56
|
return response.data[0].embedding
|
|
49
57
|
|
|
58
|
+
@retry_local
|
|
50
59
|
async def a_embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
51
|
-
embedding_model = self.load_model()
|
|
60
|
+
embedding_model = self.load_model(async_mode=True)
|
|
52
61
|
response = await embedding_model.embeddings.create(
|
|
53
62
|
model=self.model_name,
|
|
54
63
|
input=texts,
|
|
55
64
|
)
|
|
56
65
|
return [data.embedding for data in response.data]
|
|
57
66
|
|
|
67
|
+
###############################################
|
|
68
|
+
# Model
|
|
69
|
+
###############################################
|
|
70
|
+
|
|
58
71
|
def get_model_name(self):
|
|
59
72
|
return self.model_name
|
|
73
|
+
|
|
74
|
+
def load_model(self, async_mode: bool = False):
|
|
75
|
+
if not async_mode:
|
|
76
|
+
return self._build_client(OpenAI)
|
|
77
|
+
return self._build_client(AsyncOpenAI)
|
|
78
|
+
|
|
79
|
+
def _client_kwargs(self) -> Dict:
|
|
80
|
+
"""
|
|
81
|
+
If Tenacity manages retries, turn off OpenAI SDK retries to avoid double retrying.
|
|
82
|
+
If users opt into SDK retries via DEEPEVAL_SDK_RETRY_PROVIDERS=local, leave them enabled.
|
|
83
|
+
"""
|
|
84
|
+
kwargs = dict(self.kwargs or {})
|
|
85
|
+
if not sdk_retries_for(PS.LOCAL):
|
|
86
|
+
kwargs["max_retries"] = 0
|
|
87
|
+
return kwargs
|
|
88
|
+
|
|
89
|
+
def _build_client(self, cls):
|
|
90
|
+
kw = dict(
|
|
91
|
+
api_key=self.api_key,
|
|
92
|
+
base_url=self.base_url,
|
|
93
|
+
**self._client_kwargs(),
|
|
94
|
+
)
|
|
95
|
+
try:
|
|
96
|
+
return cls(**kw)
|
|
97
|
+
except TypeError as e:
|
|
98
|
+
# Older OpenAI SDKs may not accept max_retries; drop and retry once.
|
|
99
|
+
if "max_retries" in str(e):
|
|
100
|
+
kw.pop("max_retries", None)
|
|
101
|
+
return cls(**kw)
|
|
102
|
+
raise
|
|
@@ -3,6 +3,13 @@ from typing import List
|
|
|
3
3
|
|
|
4
4
|
from deepeval.key_handler import EmbeddingKeyValues, KEY_FILE_HANDLER
|
|
5
5
|
from deepeval.models import DeepEvalBaseEmbeddingModel
|
|
6
|
+
from deepeval.models.retry_policy import (
|
|
7
|
+
create_retry_decorator,
|
|
8
|
+
)
|
|
9
|
+
from deepeval.constants import ProviderSlug as PS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
retry_ollama = create_retry_decorator(PS.OLLAMA)
|
|
6
13
|
|
|
7
14
|
|
|
8
15
|
class OllamaEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
@@ -13,6 +20,7 @@ class OllamaEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
13
20
|
model_name = KEY_FILE_HANDLER.fetch_data(
|
|
14
21
|
EmbeddingKeyValues.LOCAL_EMBEDDING_MODEL_NAME
|
|
15
22
|
)
|
|
23
|
+
# TODO: This is not being used. Clean it up in consistency PR
|
|
16
24
|
self.api_key = KEY_FILE_HANDLER.fetch_data(
|
|
17
25
|
EmbeddingKeyValues.LOCAL_EMBEDDING_API_KEY
|
|
18
26
|
)
|
|
@@ -20,12 +28,7 @@ class OllamaEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
20
28
|
self.kwargs = kwargs
|
|
21
29
|
super().__init__(model_name)
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
if not async_mode:
|
|
25
|
-
return Client(host=self.base_url)
|
|
26
|
-
|
|
27
|
-
return AsyncClient(host=self.base_url)
|
|
28
|
-
|
|
31
|
+
@retry_ollama
|
|
29
32
|
def embed_text(self, text: str) -> List[float]:
|
|
30
33
|
embedding_model = self.load_model()
|
|
31
34
|
response = embedding_model.embed(
|
|
@@ -34,6 +37,7 @@ class OllamaEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
34
37
|
)
|
|
35
38
|
return response["embeddings"][0]
|
|
36
39
|
|
|
40
|
+
@retry_ollama
|
|
37
41
|
def embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
38
42
|
embedding_model = self.load_model()
|
|
39
43
|
response = embedding_model.embed(
|
|
@@ -42,6 +46,7 @@ class OllamaEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
42
46
|
)
|
|
43
47
|
return response["embeddings"]
|
|
44
48
|
|
|
49
|
+
@retry_ollama
|
|
45
50
|
async def a_embed_text(self, text: str) -> List[float]:
|
|
46
51
|
embedding_model = self.load_model(async_mode=True)
|
|
47
52
|
response = await embedding_model.embed(
|
|
@@ -50,6 +55,7 @@ class OllamaEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
50
55
|
)
|
|
51
56
|
return response["embeddings"][0]
|
|
52
57
|
|
|
58
|
+
@retry_ollama
|
|
53
59
|
async def a_embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
54
60
|
embedding_model = self.load_model(async_mode=True)
|
|
55
61
|
response = await embedding_model.embed(
|
|
@@ -58,5 +64,17 @@ class OllamaEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
58
64
|
)
|
|
59
65
|
return response["embeddings"]
|
|
60
66
|
|
|
67
|
+
###############################################
|
|
68
|
+
# Model
|
|
69
|
+
###############################################
|
|
70
|
+
|
|
71
|
+
def load_model(self, async_mode: bool = False):
|
|
72
|
+
if not async_mode:
|
|
73
|
+
return self._build_client(Client)
|
|
74
|
+
return self._build_client(AsyncClient)
|
|
75
|
+
|
|
76
|
+
def _build_client(self, cls):
|
|
77
|
+
return cls(host=self.base_url, **self.kwargs)
|
|
78
|
+
|
|
61
79
|
def get_model_name(self):
|
|
62
|
-
return self.model_name
|
|
80
|
+
return f"{self.model_name} (Ollama)"
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
from typing import Optional, List
|
|
1
|
+
from typing import Dict, Optional, List
|
|
2
2
|
from openai import OpenAI, AsyncOpenAI
|
|
3
3
|
from deepeval.models import DeepEvalBaseEmbeddingModel
|
|
4
|
+
from deepeval.models.retry_policy import (
|
|
5
|
+
create_retry_decorator,
|
|
6
|
+
sdk_retries_for,
|
|
7
|
+
)
|
|
8
|
+
from deepeval.constants import ProviderSlug as PS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
retry_openai = create_retry_decorator(PS.OPENAI)
|
|
4
12
|
|
|
5
13
|
valid_openai_embedding_models = [
|
|
6
14
|
"text-embedding-3-small",
|
|
@@ -15,6 +23,7 @@ class OpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
15
23
|
self,
|
|
16
24
|
model: Optional[str] = None,
|
|
17
25
|
_openai_api_key: Optional[str] = None,
|
|
26
|
+
**kwargs,
|
|
18
27
|
):
|
|
19
28
|
model_name = model if model else default_openai_embedding_model
|
|
20
29
|
if model_name not in valid_openai_embedding_models:
|
|
@@ -23,7 +32,9 @@ class OpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
23
32
|
)
|
|
24
33
|
self._openai_api_key = _openai_api_key
|
|
25
34
|
self.model_name = model_name
|
|
35
|
+
self.kwargs = kwargs
|
|
26
36
|
|
|
37
|
+
@retry_openai
|
|
27
38
|
def embed_text(self, text: str) -> List[float]:
|
|
28
39
|
client = self.load_model(async_mode=False)
|
|
29
40
|
response = client.embeddings.create(
|
|
@@ -32,6 +43,7 @@ class OpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
32
43
|
)
|
|
33
44
|
return response.data[0].embedding
|
|
34
45
|
|
|
46
|
+
@retry_openai
|
|
35
47
|
def embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
36
48
|
client = self.load_model(async_mode=False)
|
|
37
49
|
response = client.embeddings.create(
|
|
@@ -40,6 +52,7 @@ class OpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
40
52
|
)
|
|
41
53
|
return [item.embedding for item in response.data]
|
|
42
54
|
|
|
55
|
+
@retry_openai
|
|
43
56
|
async def a_embed_text(self, text: str) -> List[float]:
|
|
44
57
|
client = self.load_model(async_mode=True)
|
|
45
58
|
response = await client.embeddings.create(
|
|
@@ -48,6 +61,7 @@ class OpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
48
61
|
)
|
|
49
62
|
return response.data[0].embedding
|
|
50
63
|
|
|
64
|
+
@retry_openai
|
|
51
65
|
async def a_embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
52
66
|
client = self.load_model(async_mode=True)
|
|
53
67
|
response = await client.embeddings.create(
|
|
@@ -56,11 +70,39 @@ class OpenAIEmbeddingModel(DeepEvalBaseEmbeddingModel):
|
|
|
56
70
|
)
|
|
57
71
|
return [item.embedding for item in response.data]
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
###############################################
|
|
74
|
+
# Model
|
|
75
|
+
###############################################
|
|
76
|
+
|
|
77
|
+
def get_model_name(self):
|
|
60
78
|
return self.model_name
|
|
61
79
|
|
|
62
|
-
def load_model(self, async_mode: bool):
|
|
80
|
+
def load_model(self, async_mode: bool = False):
|
|
63
81
|
if not async_mode:
|
|
64
|
-
return
|
|
82
|
+
return self._build_client(OpenAI)
|
|
83
|
+
return self._build_client(AsyncOpenAI)
|
|
65
84
|
|
|
66
|
-
|
|
85
|
+
def _client_kwargs(self) -> Dict:
|
|
86
|
+
"""
|
|
87
|
+
If Tenacity is managing retries, force OpenAI SDK retries off to avoid double retries.
|
|
88
|
+
If the user opts into SDK retries for 'openai' via DEEPEVAL_SDK_RETRY_PROVIDERS,
|
|
89
|
+
leave their retry settings as is.
|
|
90
|
+
"""
|
|
91
|
+
kwargs = dict(self.kwargs or {})
|
|
92
|
+
if not sdk_retries_for(PS.OPENAI):
|
|
93
|
+
kwargs["max_retries"] = 0
|
|
94
|
+
return kwargs
|
|
95
|
+
|
|
96
|
+
def _build_client(self, cls):
|
|
97
|
+
kw = dict(
|
|
98
|
+
api_key=self._openai_api_key,
|
|
99
|
+
**self._client_kwargs(),
|
|
100
|
+
)
|
|
101
|
+
try:
|
|
102
|
+
return cls(**kw)
|
|
103
|
+
except TypeError as e:
|
|
104
|
+
# older OpenAI SDKs may not accept max_retries, in that case remove and retry once
|
|
105
|
+
if "max_retries" in str(e):
|
|
106
|
+
kw.pop("max_retries", None)
|
|
107
|
+
return cls(**kw)
|
|
108
|
+
raise
|