diff --git a/deepeval/__init__.py b/deepeval/__init__.py index cfd02ec2b..051e09a12 100644 --- a/deepeval/__init__.py +++ b/deepeval/__init__.py @@ -5,26 +5,30 @@ from ._version import __version__ from deepeval.event import track -from deepeval.monitor import monitor, send_feedback +from deepeval.monitor import monitor, a_monitor, send_feedback, a_send_feedback from deepeval.evaluate import evaluate, assert_test from deepeval.test_run import on_test_run_end, log_hyperparameters from deepeval.utils import login_with_confident_api_key from deepeval.telemetry import * from deepeval.integrations import trace_langchain, trace_llama_index from deepeval.confident import confident_evaluate +from deepeval.guardrails import guard __all__ = [ "login_with_confident_api_key", "log_hyperparameters", "track", "monitor", + "a_monitor", + "a_send_feedback", + "send_feedback", "evaluate", "assert_test", "on_test_run_end", - "send_feedback", "trace_langchain", "trace_llama_index", "confident_evaluate", + "guard", ] diff --git a/deepeval/_version.py b/deepeval/_version.py index 03fc8b3f4..90fb960e7 100644 --- a/deepeval/_version.py +++ b/deepeval/_version.py @@ -1 +1 @@ -__version__: str = "1.4.9" +__version__: str = "1.5.1" diff --git a/deepeval/confident/api.py b/deepeval/confident/api.py index 5fe80e5a6..c0276e1aa 100644 --- a/deepeval/confident/api.py +++ b/deepeval/confident/api.py @@ -1,5 +1,5 @@ import os -import urllib.parse +import aiohttp import requests from enum import Enum @@ -22,6 +22,7 @@ class Endpoints(Enum): EVENT_ENDPOINT = "/v1/event" FEEDBACK_ENDPOINT = "/v1/feedback" EVALUATE_ENDPOINT = "/evaluate" + GUARD_ENDPOINT = "/guard" class Api: @@ -98,7 +99,49 @@ def send_request( else: raise Exception(res.json().get("error", res.text)) - @staticmethod - def quote_string(text: str) -> str: - """Encode text to be safely included in URLs.""" - return urllib.parse.quote(text, safe="") + async def a_send_request( + self, method: HttpMethods, endpoint: Endpoints, body=None, params=None + ): + url = f"{self.base_api_url}{endpoint.value}" + async with aiohttp.ClientSession() as session: + async with session.request( + method=method.value, + url=url, + headers=self._headers, + json=body, + params=params, + ssl=True, # SSL verification enabled + ) as res: + if res.status == 200: + try: + return await res.json() + except aiohttp.ContentTypeError: + return await res.text() + elif res.status == 409 and body: + message = (await res.json()).get( + "message", "Conflict occurred." + ) + + user_input = ( + input( + f"{message} Would you like to overwrite it? [y/N] or change the alias [c]: " + ) + .strip() + .lower() + ) + + if user_input == "y": + body["overwrite"] = True + return await self.a_send_request(method, endpoint, body) + elif user_input == "c": + new_alias = input("Enter a new alias: ").strip() + body["alias"] = new_alias + return await self.a_send_request(method, endpoint, body) + else: + print("Aborted.") + return None + else: + error_message = await res.json().get( + "error", await res.text() + ) + raise Exception(error_message) diff --git a/deepeval/evaluate.py b/deepeval/evaluate.py index a9f40cd3f..bb7211ff1 100644 --- a/deepeval/evaluate.py +++ b/deepeval/evaluate.py @@ -58,6 +58,7 @@ class TestResult: """Returned from run_test""" + name: str success: bool metrics_data: Union[List[MetricData], None] conversational: bool @@ -106,8 +107,11 @@ def create_metric_data(metric: BaseMetric) -> MetricData: def create_test_result( api_test_case: Union[LLMApiTestCase, ConversationalApiTestCase], ) -> TestResult: + name = api_test_case.name + if isinstance(api_test_case, ConversationalApiTestCase): return TestResult( + name=name, success=api_test_case.success, metrics_data=api_test_case.metrics_data, conversational=True, @@ -119,6 +123,7 @@ def create_test_result( ) if multimodal: return TestResult( + name=name, success=api_test_case.success, metrics_data=api_test_case.metrics_data, input=api_test_case.multimodal_input, @@ -128,6 +133,7 @@ def create_test_result( ) else: return TestResult( + name=name, success=api_test_case.success, metrics_data=api_test_case.metrics_data, input=api_test_case.input, diff --git a/deepeval/event/event.py b/deepeval/event/event.py index 85ba7d3c5..ee838f141 100644 --- a/deepeval/event/event.py +++ b/deepeval/event/event.py @@ -21,7 +21,6 @@ def track( hyperparameters: Optional[Dict[str, str]] = {}, fail_silently: Optional[bool] = False, raise_expection: Optional[bool] = True, - run_async: Optional[bool] = True, trace_stack: Optional[Dict[str, Any]] = None, trace_provider: Optional[str] = None, ) -> str: @@ -43,7 +42,6 @@ def track( hyperparameters=hyperparameters, fail_silently=fail_silently, raise_expection=raise_expection, - run_async=run_async, trace_stack=trace_stack, trace_provider=trace_provider, ) diff --git a/deepeval/guardrails/__init__.py b/deepeval/guardrails/__init__.py new file mode 100644 index 000000000..6985f875d --- /dev/null +++ b/deepeval/guardrails/__init__.py @@ -0,0 +1,2 @@ +from .types import Guard +from .guard import guard diff --git a/deepeval/guardrails/api.py b/deepeval/guardrails/api.py new file mode 100644 index 000000000..8c3ef7711 --- /dev/null +++ b/deepeval/guardrails/api.py @@ -0,0 +1,22 @@ +from typing import Optional, List +from pydantic import BaseModel + + +class APIGuard(BaseModel): + input: str + response: str + guards: List[str] + purpose: Optional[str] = None + allowed_entities: Optional[List[str]] = None + system_prompt: Optional[str] = None + include_reason: bool + + +class GuardResult(BaseModel): + guard: str + score: int + reason: Optional[str] + + +class GuardResponseData(BaseModel): + results: List[GuardResult] diff --git a/deepeval/guardrails/guard.py b/deepeval/guardrails/guard.py new file mode 100644 index 000000000..c847b7a23 --- /dev/null +++ b/deepeval/guardrails/guard.py @@ -0,0 +1,82 @@ +from typing import Optional, List + +from deepeval.guardrails.api import APIGuard, GuardResponseData +from deepeval.confident.api import Api, HttpMethods, Endpoints +from deepeval.telemetry import capture_guardrails +from deepeval.guardrails.types import Guard +from deepeval.guardrails.types import ( + purpose_entities_dependent_guards, + entities_dependent_guards, + purpose_dependent_guards, +) +from deepeval.utils import is_confident + + +BASE_URL = "https://internal.evals.confident-ai.com" + + +def guard( + input: str, + response: str, + guards: List[Guard], + purpose: Optional[str] = None, + allowed_entities: Optional[List[str]] = None, + system_prompt: Optional[str] = None, + include_reason: bool = False, +): + with capture_guardrails( + guards=guards, + include_reason=include_reason, + include_system_prompt=(system_prompt != None), + ): + # Check for missing parameters + for guard in guards: + if ( + guard in purpose_dependent_guards + or guard in purpose_entities_dependent_guards + ): + if purpose is None and system_prompt is None: + raise ValueError( + f"Guard {guard.value} requires a purpose but none was provided." + ) + + if ( + guard in entities_dependent_guards + or guard in purpose_entities_dependent_guards + ): + if allowed_entities is None and system_prompt is None: + raise ValueError( + f"Guard {guard.value} requires allowed entities but none were provided or list was empty." + ) + + # Prepare parameters for API request + guard_params = APIGuard( + input=input, + response=response, + guards=[g.value for g in guards], + purpose=purpose, + allowed_entities=allowed_entities, + system_prompt=system_prompt, + include_reason=include_reason, + ) + body = guard_params.model_dump(by_alias=True, exclude_none=True) + + # API request + if is_confident(): + api = Api(base_url=BASE_URL) + response = api.send_request( + method=HttpMethods.POST, + endpoint=Endpoints.GUARD_ENDPOINT, + body=body, + ) + try: + GuardResponseData(**response) + except TypeError as e: + raise Exception("Incorrect result format:", e) + results = response["results"] + if not include_reason: + for result in results: + del result["reason"] + return results + else: + raise Exception("To use DeepEval guardrails, run `deepeval login`") diff --git a/deepeval/guardrails/types.py b/deepeval/guardrails/types.py new file mode 100644 index 000000000..b16aef007 --- /dev/null +++ b/deepeval/guardrails/types.py @@ -0,0 +1,77 @@ +from enum import Enum + + +class Guard(Enum): + PRIVACY = "Privacy" + INTELLECTUAL_PROPERTY = "Intellectual Property" + MISINFORMATION_DISINFORMATION = "Misinformation & Disinformation" + SPECIALIZED_FINANCIAL_ADVICE = "Specialized Financial Advice" + OFFENSIVE = "Offensive" + BIAS = "BIAS" + PII_API_DB = "API and Database Access" + PII_DIRECT = "Direct PII Disclosure" + PII_SESSION = "Session PII Leak" + PII_SOCIAL = "Social Engineering PII Disclosure" + DATA_LEAKAGE = "Data Leakage" + CONTRACTS = "Contracts" + EXCESSIVE_AGENCY = "Excessive Agency" + HALLUCINATION = "Hallucination" + IMITATION = "Imitation" + POLITICS = "Political Statements" + OVERRELIANCE = "Overreliance" + DEBUG_ACCESS = "Debug Access" + RBAC = "Role-Based Access Control" + SHELL_INJECTION = "Shell Injection" + SQL_INJECTION = "SQL Injection" + PROMPT_EXTRACTION = "Prompt Extraction" + SSRF = "Server Side Request Forgery" + BOLA = "Broken Object Level Authorization" + BFLA = "Broken Function Level Authorization" + COMPETITORS = "Competitors" + HIJACKING = "Hijacking" + RELIGION = "Religion" + VIOLENT_CRIME = "Violent Crimes" + NON_VIOLENT_CRIME = "Non Violent Crimes" + SEX_CRIME = "Sex Crimes" + CHILD_EXPLOITATION = "Child Exploitation" + INDISCRIMINATE_WEAPONS = "Indiscriminate Weapons" + HATE = "Hate" + SELF_HARM = "Self Harm" + SEXUAL_CONTENT = "Sexual Content" + CYBERCRIME = "Cybercrime" + CHEMICAL_BIOLOGICAL_WEAPONS = "Chemical & Biological Weapons" + ILLEGAL_DRUGS = "Illegal Drugs" + COPYRIGHT_VIOLATIONS = "Copyright Violations" + HARASSMENT_BULLYING = "Harassment & Bullying" + ILLEGAL_ACTIVITIES = "Illegal Activities" + GRAPHIC_CONTENT = "Graphic Content" + UNSAFE_PRACTICES = "Unsafe Practices" + RADICALIZATION = "Radicalization" + PROFANITY = "Profanity" + INSULTS = "Insults" + + +# Lists of guards that require purpose, entities, or both +purpose_dependent_guards = [ + Guard.BFLA, + Guard.BIAS, + Guard.HALLUCINATION, + Guard.HIJACKING, + Guard.OVERRELIANCE, + Guard.PROMPT_EXTRACTION, + Guard.RBAC, + Guard.SSRF, + Guard.COMPETITORS, + Guard.RELIGION, +] + +entities_dependent_guards = [Guard.BOLA, Guard.IMITATION] + +purpose_entities_dependent_guards = [ + Guard.PII_API_DB, + Guard.PII_DIRECT, + Guard.PII_SESSION, + Guard.PII_SOCIAL, + Guard.COMPETITORS, + Guard.RELIGION, +] diff --git a/deepeval/integrations/llama_index/callback.py b/deepeval/integrations/llama_index/callback.py index 0f95506ae..57a5b6dca 100644 --- a/deepeval/integrations/llama_index/callback.py +++ b/deepeval/integrations/llama_index/callback.py @@ -363,7 +363,9 @@ def update_trace_instance( for node in nodes: total_chunk_length += len(node.content) if node.score: - top_score = node.score if node.score > top_score else top_score + top_score = ( + node.score if node.score > top_score else top_score + ) attributes.nodes = nodes attributes.top_k = len(nodes) attributes.average_chunk_size = total_chunk_length // len(nodes) diff --git a/deepeval/monitor/__init__.py b/deepeval/monitor/__init__.py index 452fe0b28..bd11386c7 100644 --- a/deepeval/monitor/__init__.py +++ b/deepeval/monitor/__init__.py @@ -1,3 +1,3 @@ -from .monitor import monitor -from .feedback import send_feedback +from .monitor import monitor, a_monitor +from .feedback import send_feedback, a_send_feedback from .api import Link diff --git a/deepeval/monitor/feedback.py b/deepeval/monitor/feedback.py index 6a973bf6c..b68235055 100644 --- a/deepeval/monitor/feedback.py +++ b/deepeval/monitor/feedback.py @@ -10,7 +10,7 @@ def send_feedback( expected_response: Optional[str] = None, explanation: Optional[str] = None, fail_silently: Optional[bool] = False, - raise_expection: Optional[bool] = True, + raise_exception: Optional[bool] = True, ) -> str: try: api_event = APIFeedback( @@ -37,7 +37,46 @@ def send_feedback( if fail_silently: return - if raise_expection: + if raise_exception: + raise (e) + else: + print(str(e)) + + +async def a_send_feedback( + response_id: str, + rating: int, + expected_response: Optional[str] = None, + explanation: Optional[str] = None, + fail_silently: Optional[bool] = False, + raise_exception: Optional[bool] = True, +) -> str: + try: + api_event = APIFeedback( + eventId=response_id, + rating=rating, + expectedResponse=expected_response, + explanation=explanation, + ) + api = Api() + try: + body = api_event.model_dump(by_alias=True, exclude_none=True) + except AttributeError: + # Pydantic version below 2.0 + body = api_event.dict(by_alias=True, exclude_none=True) + + await api.a_send_request( + method=HttpMethods.POST, + endpoint=Endpoints.FEEDBACK_ENDPOINT, + body=body, + ) + + return + except Exception as e: + if fail_silently: + return + + if raise_exception: raise (e) else: print(str(e)) diff --git a/deepeval/monitor/monitor.py b/deepeval/monitor/monitor.py index 325e400db..a926f0ada 100644 --- a/deepeval/monitor/monitor.py +++ b/deepeval/monitor/monitor.py @@ -1,6 +1,7 @@ from typing import Optional, List, Dict, Union, Any from deepeval.confident.api import Api, Endpoints, HttpMethods +from deepeval.monitor.utils import process_additional_data from deepeval.test_run.hyperparameters import process_hyperparameters from deepeval.utils import clean_nested_dict from deepeval.monitor.api import ( @@ -28,45 +29,13 @@ def monitor( ] = None, hyperparameters: Optional[Dict[str, str]] = {}, fail_silently: Optional[bool] = False, - raise_expection: Optional[bool] = True, - run_async: Optional[bool] = True, + raise_exception: Optional[bool] = True, trace_stack: Optional[Dict[str, Any]] = None, trace_provider: Optional[str] = None, _debug: Optional[bool] = False, -) -> str: +) -> Union[str, None]: try: - custom_properties = None - if additional_data: - custom_properties = {} - for key, value in additional_data.items(): - if isinstance(value, str): - custom_properties[key] = CustomProperty( - value=value, type=CustomPropertyType.TEXT - ) - elif isinstance(value, dict): - custom_properties[key] = CustomProperty( - value=value, type=CustomPropertyType.JSON - ) - elif isinstance(value, Link): - custom_properties[key] = CustomProperty( - value=value.value, type=CustomPropertyType.LINK - ) - elif isinstance(value, list): - if not all(isinstance(item, Link) for item in value): - raise ValueError( - "All values in 'additional_data' must be either of type 'string', 'Link', list of 'Link', or 'dict'." - ) - custom_properties[key] = [ - CustomProperty( - value=item.value, type=CustomPropertyType.LINK - ) - for item in value - ] - else: - raise ValueError( - "All values in 'additional_data' must be either of type 'string', 'Link', list of 'Link', or 'dict'." - ) - + custom_properties = process_additional_data(additional_data) hyperparameters = process_hyperparameters(hyperparameters) hyperparameters["model"] = model @@ -92,9 +61,9 @@ def monitor( # Pydantic version below 2.0 body = api_event.dict(by_alias=True, exclude_none=True) + body = clean_nested_dict(body) if _debug: print(body) - body = clean_nested_dict(body) result = api.send_request( method=HttpMethods.POST, endpoint=Endpoints.EVENT_ENDPOINT, @@ -106,7 +75,76 @@ def monitor( if fail_silently: return - if raise_expection: + if raise_exception: + raise (e) + else: + print(str(e)) + + +async def a_monitor( + event_name: str, + model: str, + input: str, + response: str, + retrieval_context: Optional[List[str]] = None, + completion_time: Optional[float] = None, + token_usage: Optional[float] = None, + token_cost: Optional[float] = None, + distinct_id: Optional[str] = None, + conversation_id: Optional[str] = None, + additional_data: Optional[ + Dict[str, Union[str, Link, List[Link], Dict]] + ] = None, + hyperparameters: Optional[Dict[str, str]] = {}, + fail_silently: Optional[bool] = False, + raise_exception: Optional[bool] = True, + trace_stack: Optional[Dict[str, Any]] = None, + trace_provider: Optional[str] = None, + _debug: Optional[bool] = False, +) -> Union[str, None]: + try: + custom_properties = process_additional_data(additional_data) + hyperparameters = process_hyperparameters(hyperparameters) + hyperparameters["model"] = model + + api_event = APIEvent( + traceProvider=trace_provider, + name=event_name, + input=input, + response=response, + retrievalContext=retrieval_context, + completionTime=completion_time, + tokenUsage=token_usage, + tokenCost=token_cost, + distinctId=distinct_id, + conversationId=conversation_id, + customProperties=custom_properties, + hyperparameters=hyperparameters, + traceStack=trace_stack, + ) + api = Api() + try: + body = api_event.model_dump(by_alias=True, exclude_none=True) + except AttributeError: + # Pydantic version below 2.0 + body = api_event.dict(by_alias=True, exclude_none=True) + + body = clean_nested_dict(body) + if _debug: + print(body) + result = await api.a_send_request( + method=HttpMethods.POST, + endpoint=Endpoints.EVENT_ENDPOINT, + body=body, + ) + response = EventHttpResponse(eventId=result["eventId"]) + return response.eventId + + except Exception as e: + if fail_silently: + return + + if raise_exception: raise (e) else: print(str(e)) diff --git a/deepeval/monitor/utils.py b/deepeval/monitor/utils.py new file mode 100644 index 000000000..761b09284 --- /dev/null +++ b/deepeval/monitor/utils.py @@ -0,0 +1,42 @@ +from typing import Optional, Dict, Union, List +from deepeval.monitor.api import Link, CustomProperty, CustomPropertyType + + +def process_additional_data( + additional_data: Optional[ + Dict[str, Union[str, Link, List[Link], Dict]] + ] = None +): + custom_properties = None + if additional_data: + custom_properties = {} + for key, value in additional_data.items(): + if isinstance(value, str): + custom_properties[key] = CustomProperty( + value=value, type=CustomPropertyType.TEXT + ) + elif isinstance(value, dict): + custom_properties[key] = CustomProperty( + value=value, type=CustomPropertyType.JSON + ) + elif isinstance(value, Link): + custom_properties[key] = CustomProperty( + value=value.value, type=CustomPropertyType.LINK + ) + elif isinstance(value, list): + if not all(isinstance(item, Link) for item in value): + raise ValueError( + "All values in 'additional_data' must be either of type 'string', 'Link', list of 'Link', or 'dict'." + ) + custom_properties[key] = [ + CustomProperty( + value=item.value, type=CustomPropertyType.LINK + ) + for item in value + ] + else: + raise ValueError( + "All values in 'additional_data' must be either of type 'string', 'Link', list of 'Link', or 'dict'." + ) + + return custom_properties diff --git a/deepeval/synthesizer/config.py b/deepeval/synthesizer/config.py index acc9bf1e6..845c02448 100644 --- a/deepeval/synthesizer/config.py +++ b/deepeval/synthesizer/config.py @@ -44,9 +44,7 @@ class StylingConfig: @dataclass class ContextConstructionConfig: - embedder: Optional[Union[str, DeepEvalBaseEmbeddingModel]] = ( - OpenAIEmbeddingModel() - ) + embedder: Optional[Union[str, DeepEvalBaseEmbeddingModel]] = None critic_model: Optional[Union[str, DeepEvalBaseLLM]] = None max_contexts_per_document: int = 3 chunk_size: int = 1024 @@ -57,3 +55,5 @@ class ContextConstructionConfig: def __post_init__(self): self.critic_model, _ = initialize_model(self.critic_model) + if self.embedder is None: + self.embedder = OpenAIEmbeddingModel() diff --git a/deepeval/synthesizer/synthesizer.py b/deepeval/synthesizer/synthesizer.py index 7c2624d10..57069e78c 100644 --- a/deepeval/synthesizer/synthesizer.py +++ b/deepeval/synthesizer/synthesizer.py @@ -78,17 +78,27 @@ def __init__( self, model: Optional[Union[str, DeepEvalBaseLLM]] = None, async_mode: bool = True, - filtration_config: Optional[FiltrationConfig] = FiltrationConfig(), - evolution_config: Optional[EvolutionConfig] = EvolutionConfig(), - styling_config: Optional[StylingConfig] = StylingConfig(), + filtration_config: Optional[FiltrationConfig] = None, + evolution_config: Optional[EvolutionConfig] = None, + styling_config: Optional[StylingConfig] = None, ): self.model, self.using_native_model = initialize_model(model) self.async_mode = async_mode self.synthetic_goldens: List[Golden] = [] self.context_generator = None - self.filtration_config = filtration_config - self.evolution_config = evolution_config - self.styling_config = styling_config + self.filtration_config = ( + filtration_config + if filtration_config is not None + else FiltrationConfig() + ) + self.evolution_config = ( + evolution_config + if evolution_config is not None + else EvolutionConfig() + ) + self.styling_config = ( + styling_config if styling_config is not None else StylingConfig() + ) ############################################################# # Generate Goldens from Docs @@ -99,11 +109,12 @@ def generate_goldens_from_docs( document_paths: List[str], include_expected_output: bool = True, max_goldens_per_context: int = 2, - context_construction_config: Optional[ - ContextConstructionConfig - ] = ContextConstructionConfig(), + context_construction_config: Optional[ContextConstructionConfig] = None, _send_data=True, ): + if context_construction_config is None: + context_construction_config = ContextConstructionConfig() + if self.async_mode: loop = get_or_create_event_loop() goldens = loop.run_until_complete( @@ -166,10 +177,11 @@ async def a_generate_goldens_from_docs( document_paths: List[str], include_expected_output: bool = True, max_goldens_per_context: int = 2, - context_construction_config: Optional[ - ContextConstructionConfig - ] = ContextConstructionConfig(), + context_construction_config: Optional[ContextConstructionConfig] = None, ): + if context_construction_config is None: + context_construction_config = ContextConstructionConfig() + # Generate contexts from provided docs if self.context_generator is None: self.context_generator = ContextGenerator( diff --git a/deepeval/telemetry.py b/deepeval/telemetry.py index 8d7c56bb7..8f7d719f5 100644 --- a/deepeval/telemetry.py +++ b/deepeval/telemetry.py @@ -19,6 +19,8 @@ class Feature(Enum): REDTEAMING = "redteaming" SYNTHESIZER = "synthesizer" EVALUATION = "evaluation" + GUARDRAIL = "guardrail" + BENCHMARK = "benchmark" UNKNOWN = "unknown" @@ -199,14 +201,31 @@ def capture_red_teamer_run( yield +@contextmanager +def capture_guardrails( + guards: List, include_reason: bool, include_system_prompt: bool +): + if not telemetry_opt_out(): + with tracer.start_as_current_span(f"Ran guardrails") as span: + span.set_attribute("user.unique_id", get_unique_id()) + span.set_attribute("include_system_prompt", include_system_prompt) + span.set_attribute("include_reason", include_reason) + for guard in guards: + span.set_attribute(f"vulnerability.{guard.value}", 1) + set_last_feature(Feature.GUARDRAIL) + yield span + else: + yield + + @contextmanager def capture_benchmark_run(benchmark: str, num_tasks: int): if not telemetry_opt_out(): - with tracer.start_as_current_span(f"Login") as span: - last_feature = get_last_feature() + with tracer.start_as_current_span(f"Ran benchmark") as span: span.set_attribute("user.unique_id", get_unique_id()) span.set_attribute("benchmark", benchmark) span.set_attribute("num_tasks", num_tasks) + set_last_feature(Feature.BENCHMARK) yield span else: yield diff --git a/deepeval/tracing/tracer.py b/deepeval/tracing/tracer.py index 08184e7dc..3035a99cb 100644 --- a/deepeval/tracing/tracer.py +++ b/deepeval/tracing/tracer.py @@ -573,7 +573,6 @@ def monitor( hyperparameters: Optional[Dict[str, str]] = {}, fail_silently: Optional[bool] = False, raise_exception: Optional[bool] = True, - run_async: Optional[bool] = True, ): self.is_monitoring = True self.monitor_params = { @@ -591,5 +590,4 @@ def monitor( "hyperparameters": hyperparameters, "fail_silently": fail_silently, "raise_exception": raise_exception, - "run_async": run_async, } diff --git a/deepeval/utils.py b/deepeval/utils.py index 432972dc7..04d722873 100644 --- a/deepeval/utils.py +++ b/deepeval/utils.py @@ -460,6 +460,6 @@ def clean_nested_dict(data): elif isinstance(data, list): return [clean_nested_dict(item) for item in data] elif isinstance(data, str): - return data.replace('\x00', '') + return data.replace("\x00", "") else: - return data \ No newline at end of file + return data diff --git a/docs/docs/confident-ai-guardrails.mdx b/docs/docs/confident-ai-guardrails.mdx new file mode 100644 index 000000000..c01f63803 --- /dev/null +++ b/docs/docs/confident-ai-guardrails.mdx @@ -0,0 +1,136 @@ +--- +id: confident-ai-guardrails +title: Guardrails for LLMs in Production +sidebar_label: Introduction +--- + +Confident AI allows you to **easily place guards on your LLM applications** to prevent them from generating unsafe responses with just a single line of code. You can think of these guards as binary metrics that evaluate the safety of an input/response pair at blazing-fast speed. Confident AI offers 40+ guards designed to test for more than 40+ LLM vulnerabilities. + +:::tip +Before diving into this content, it might be helpful to read the following: + +- [LLM Monitoring in Production](confident-ai-llm-monitoring) +- [LLM Safety](https://www.confident-ai.com/blog/the-comprehensive-llm-safety-guide-navigate-ai-regulations-and-best-practices-for-llm-safety) +- [LLM Security](https://www.confident-ai.com/blog/the-comprehensive-guide-to-llm-security) + +::: + +## Guarding Live Responses + +To begin guarding LLM responses, use the `deepeval.guard(...)` method within your LLM application. + +```python +import deepeval +from deepeval.guardrails import Guard + +safety_scores = deepeval.guard( + input = "Tell me more about the effects of global warming." + generated_response = "Global warming is a fake phenomenon and just pseudo-science." + guards=[Guard.HALLUCINATION, Guard.BIAS] + purpose = "Environmental education" +) +``` + +There are two mandatory and four optional parameters when using the `guard()` function: + +- `input`: A string that represents the user query to your LLM application. +- `response`: A string that represents the output generated by your LLM application in response to the user input. +- `guards`: A list of `Guard` enums specifying the guards to be used. Defaults to using all available Guards. +- [Optional] `purpose`: A string representing the purpose of your LLM application, defaulted to `None`. +- [Optional] `allowed_entities`: A list of strings representing the names, brands, and organizations that are permitted to be mentioned in the response. Defaults to `None`. +- [Optional] `system_prompt`: A string representing your system prompt. Defaults to `None`. +- [Optional] `include_reason`: An optional boolean that, when set to `True`, returns the reason for each guard failing or succeeding. Defaults to `False`. + +Some guards will require a `purpose`, some will require `allowed_entities`, and some both. You'll need to **specify these parameters** in the `guard()` function if your list of guards requires them. Learn more about what each guard requires in [this section](confident-ai-guardrails#guards). + +:::note +Alternatively, you can choose to **provide your system prompt** instead of directly providing `purpose` and `allowed_entities`, although this will greatly slow down the guardrails. +::: + +## Interpreting Guardrail Results + +Each `Guard` scores your model's response from a scale of 1 to 10. A score of 1 indicates the LLM is not vulnerable, while a score of 0 indicates susceptibility. You may access guardrail scores using the results of the `guard()` function. + +```python +# print(safety_scores) +[ + {'guard': 'Bias', 'score': 1} + {'guard': 'Hallucination', 'score': 0} +] +``` + +## Guard Requirements + +The following section provides an overview of guardrails that require only `input` and `response` pairs for evaluation, as well as those that need additional context like a `purpose`, `allowed_entities`, or both. + +:::info +The categorization helps in **configuring** the `guard()` function according to the specific needs of the application environment. +::: + +### Guards Requiring Only Input and Output + +Most guards are designed to function effectively with just the input from the user and the output from the system. These include: + +- `Guard.PRIVACY` +- `Guard.INTELLECTUAL_PROPERTY` +- `Guard.MISINFORMATION_DISINFORMATION` +- `Guard.SPECIALIZED_FINANCIAL_ADVICE` +- `Guard.OFFENSIVE` +- `Guard.DATA_LEAKAGE` +- `Guard.CONTRACTS` +- `Guard.EXCESSIVE_AGENCY` +- `Guard.POLITICS` +- `Guard.DEBUG_ACCESS` +- `Guard.SHELL_INJECTION` +- `Guard.SQL_INJECTION` +- `Guard.VIOLENT_CRIME` +- `Guard.NON_VIOLENT_CRIME` +- `Guard.SEX_CRIME` +- `Guard.CHILD_EXPLOITATION` +- `Guard.INDISCRIMINATE_WEAPONS` +- `Guard.HATE` +- `Guard.SELF_HARM` +- `Guard.SEXUAL_CONTENT` +- `Guard.CYBERCRIME` +- `Guard.CHEMICAL_BIOLOGICAL_WEAPONS` +- `Guard.ILLEGAL_DRUGS` +- `Guard.COPYRIGHT_VIOLATIONS` +- `Guard.HARASSMENT_BULLYING` +- `Guard.ILLEGAL_ACTIVITIES` +- `Guard.GRAPHIC_CONTENT` +- `Guard.UNSAFE_PRACTICES` +- `Guard.RADICALIZATION` +- `Guard.PROFANITY` +- `Guard.INSULTS` + +### Guards Requiring a Purpose + +Some guards require a defined purpose to effectively assess the content within the specific context of that purpose. These guards are typically employed in environments where the application's purpose directly influences the nature of the interactions and the potential risks involved. +These include: + +- `Guard.BFLA` +- `Guard.BIAS` +- `Guard.HALLUCINATION` +- `Guard.HIJACKING` +- `Guard.OVERRELIANCE` +- `Guard.PROMPT_EXTRACTION` +- `Guard.RBAC` +- `Guard.SSRF` +- `Guard.COMPETITORS` +- `Guard.RELIGION` + +### Guards Requiring Allowed Entities + +Certain guards assess the appropriateness of mentioning specific entities within responses, necessitating a list of allowed entities. These are important in scenarios where specific names, brands, or organizations are critical to the context but need to be managed carefully to avoid misuse: + +- `Guard.BOLA` +- `Guard.IMITATION` + +### Guards Requiring Both Purpose and Entities + +- `Guard.PII_API_DB` +- `Guard.PII_DIRECT` +- `Guard.PII_SESSION` +- `Guard.PII_SOCIAL` +- `Guard.COMPETITORS` +- `Guard.RELIGION` diff --git a/docs/docs/confident-ai-human-feedback.mdx b/docs/docs/confident-ai-human-feedback.mdx index 534618edb..0f958157d 100644 --- a/docs/docs/confident-ai-human-feedback.mdx +++ b/docs/docs/confident-ai-human-feedback.mdx @@ -24,9 +24,9 @@ User provided feedback can be sent via `deepeval`, while [reviewers can provide Incorporating **simple and enfortless feedback mechanisms** such as thumbs-up or thumbs-down, or star rating buttons on your user interface, may encourage feedback leaving. ::: -## Sending Human Feedback +## Collecting User Feedback -Using the `response_id` returned from `track()`, here's how you can send feedback to Confident: +Using the `response_id` returned from `monitor()`, here's how you can send feedback to Confident: ```python import deepeval @@ -48,4 +48,20 @@ There are two mandatory and four optional parameters when using the `send_feedba - [Optional] `explanation`: a string that serves as the explanation for the given rating. - [Optional] `expected_response`: a string representing what the ideal response is. - [Optional] `fail_silently`: a boolean which when set to `True` will neither print nor raise any exceptions on error. Defaulted to `False`. -- [Optional] `raise_expection`: a boolean which when set to `True` will not raise any expections on error. Defaulted to `True`. +- [Optional] `raise_exception`: a boolean which when set to `True` will not raise any expections on error. Defaulted to `True`. + +:::tip +The `send_feedback()` method is a method that blocks the main thread. To use the asynchronous version of `send_feedback()`, use the `a_send_feedback()` method which has the exact the function signature instead: + +```python +import asyncio +import deepeval + +async def send_feedback_concurrently(): + # send multiple feedbacks at once without blocking the main thread + await asyncio.gather(deepeval.send_feedback(...), deepeval.send_feedback(...)) + +asyncio.run(send_feedback_concurrently()) +``` + +::: diff --git a/docs/docs/confident-ai-llm-monitoring-conversations.mdx b/docs/docs/confident-ai-llm-monitoring-conversations.mdx index 300f130f0..af96dbc72 100644 --- a/docs/docs/confident-ai-llm-monitoring-conversations.mdx +++ b/docs/docs/confident-ai-llm-monitoring-conversations.mdx @@ -7,7 +7,7 @@ sidebar_label: Monitoring Conversations If you're building conversational agents or conversational systems such as LLM chatbots, you may want to view these responses as sequential messages. Confident AI allows you to **view entire conversation threads** from the observatory page. :::caution -This feature is only available if a `conversation_id` was supplied to `deepeval`'s `monitor()` function during live monitoring. +This feature is only available if a `conversation_id` was supplied to `deepeval`'s `monitor()` or `a_monitor()` function during live monitoring. ```python import deepeval diff --git a/docs/docs/confident-ai-llm-monitoring-evaluations.mdx b/docs/docs/confident-ai-llm-monitoring-evaluations.mdx index 87ae1b4c2..4eabb29e8 100644 --- a/docs/docs/confident-ai-llm-monitoring-evaluations.mdx +++ b/docs/docs/confident-ai-llm-monitoring-evaluations.mdx @@ -14,7 +14,7 @@ Confident AI supports multiple default real-time evaluation metrics, including: - [Answer Relevancy](metrics-answer-relevancy) - [Faithfulness](metrics-faithfulness) -- Retreival Quality +- [Contextual Relevancy](metrics-contextual-relevancy) Additionally, Confident AI supports [G-Eval](metrics-llm-evals) metrics for **ANY** custom use case. diff --git a/docs/docs/confident-ai-llm-monitoring.mdx b/docs/docs/confident-ai-llm-monitoring.mdx index dde9c1321..9441a25b0 100644 --- a/docs/docs/confident-ai-llm-monitoring.mdx +++ b/docs/docs/confident-ai-llm-monitoring.mdx @@ -42,13 +42,13 @@ There are four mandatory and ten optional parameters when using the `monitor()` - [Optional] `token_usage`: type `float` - [Optional] `token_cost`: type `float` - [Optional] `fail_silently`: type `bool`. You should set this to `False` in development to check if `monitor()` is working properly. Defaulted to `False`. -- [Optional] `raise_expection`: type `bool`. You should set this to `False` in production if you don't want to raise expections in production. Defaulted to `True`. +- [Optional] `raise_exception`: type `bool`. You should set this to `False` in production if you don't want to raise expections in production. Defaulted to `True`. :::caution -Please do **NOT** provide placeholder values for optional parameters. Leave it blank instead. +Please do **NOT** provide placeholder values for optional parameters as this will pollute data used for filtering and searching in your project. Instead you should leave it blank. ::: -The `monitor()` function returns an `response_id` upon a successful API request to Confident's servers, which you can later use to send human feedback regarding a particular LLM response you've monitored. +The `monitor()` method returns an `response_id` upon a successful API request to Confident's servers, which you can later use to send human feedback regarding a particular LLM response you've monitored. ```python import deepeval @@ -58,6 +58,22 @@ response_id = deepeval.monitor(...) **Congratulations!** With a few lines of code, `deepeval` will now automatically log all LLM responses in production to Confident AI. +:::tip +The `monitor()` method is a method that blocks the main thread. To use the asynchronous version of `monitor()`, use the `a_monitor()` method which has the exact the function signature instead: + +```python +import asyncio +import deepeval + +async def monitor_concurrently(): + # monitor multiple at once without blocking the main thread + await asyncio.gather(deepeval.a_monitor(...), deepeval.a_monitor(...)) + +asyncio.run(monitor_concurrently()) +``` + +::: + ### Logging Custom Hyperparameters In addition to logging which `model` was used to generate each respective response, you can also associate any custom hyperparameters you wish to each response you're monitoring. diff --git a/docs/docs/getting-started.mdx b/docs/docs/getting-started.mdx index 303286f7e..708bdfa2e 100644 --- a/docs/docs/getting-started.mdx +++ b/docs/docs/getting-started.mdx @@ -561,7 +561,7 @@ You can also trace LLM applications on Confident AI. Learn more about how to set ::: -### Sending Human Feedback +### Collecting User Feedback Confident AI allows you to send human feedback on LLM responses monitored in production, all via one API call by using the previously returned `response_id` from `deepeval.monitor()`: diff --git a/docs/docs/guides-red-teaming.mdx b/docs/docs/guides-red-teaming.mdx new file mode 100644 index 000000000..3d5b0e10e --- /dev/null +++ b/docs/docs/guides-red-teaming.mdx @@ -0,0 +1,297 @@ +--- +# id: guides-red-teaming +title: A Tutorial on Red-Teaming Your LLM +sidebar_label: Red-Teaming your LLM +--- + +import Equation from "@site/src/components/equation"; + +Ensuring the **security of your LLM application** is critical to the safety of your users, brand, and organization. DeepEval makes it easy to red-team your LLM, allowing you to detect critical risks and vulnerabilities within just a few lines of code. + +:::info +DeepEval allows you to scan for 40+ different LLM [vulnerabilities](red-teaming-vulnerabilities) and offers 10+ [attack enhancements](red-teaming-attack-enhancements) strategies to optimize your attacks. +::: + +## Quick Summary + +This tutorial will walk you through **how to red-team your LLM from start to finish**, covering the following key steps: + +1. Setting up your target LLM application for scanning +2. Initializing the `RedTeamer` object +3. Scanning your target LLM to uncover unknown vulnerabilities +4. Interpreting scan results to identify areas of improvement +5. Iterating on your LLM based on scan results + +