From 8be6ab65bec27b738993a8eb5edd89a99c40fa24 Mon Sep 17 00:00:00 2001 From: Abram Date: Sun, 10 Dec 2023 14:24:58 +0100 Subject: [PATCH 001/267] Feat - introduce func response type --- agenta-cli/agenta/sdk/types.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/agenta-cli/agenta/sdk/types.py b/agenta-cli/agenta/sdk/types.py index 8c22032bf8..a18680ecb9 100644 --- a/agenta-cli/agenta/sdk/types.py +++ b/agenta-cli/agenta/sdk/types.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from pydantic import BaseModel, Extra, HttpUrl @@ -10,6 +10,19 @@ def __init__(self, file_name: str, file_path: str): self.file_path = file_path +class FuncTokenUsage(BaseModel): + completion_tokens: str + prompt_tokens: str + total_tokens: str + + +class FuncResponse(BaseModel): + message: str + usage: Optional[FuncTokenUsage] + cost: Optional[str] + latency: str + + class DictInput(dict): def __new__(cls, default_keys=None): instance = super().__new__(cls, default_keys) From e9356b3aa857d1508807fff78228c87765b3a8d0 Mon Sep 17 00:00:00 2001 From: Abram Date: Sun, 10 Dec 2023 14:27:21 +0100 Subject: [PATCH 002/267] Update - improve execute_function and entrypoint functions --- agenta-cli/agenta/sdk/agenta_decorator.py | 45 +++++++++++++++++------ 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/agenta-cli/agenta/sdk/agenta_decorator.py b/agenta-cli/agenta/sdk/agenta_decorator.py index e132b084a1..f9893dac67 100644 --- a/agenta-cli/agenta/sdk/agenta_decorator.py +++ b/agenta-cli/agenta/sdk/agenta_decorator.py @@ -4,16 +4,17 @@ import inspect import os import sys +import time import traceback from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Any, Callable, Dict, Optional, Tuple, Union, TypeVar -import agenta +from fastapi.responses import JSONResponse from fastapi import Body, FastAPI, UploadFile from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse +import agenta from .context import save_context from .router import router as router from .types import ( @@ -26,9 +27,11 @@ TextParam, MessagesInput, FileInputURL, + FuncResponse, ) app = FastAPI() +T = TypeVar("T") origins = [ "*", @@ -52,7 +55,7 @@ def ingest_file(upfile: UploadFile): return InFile(file_name=upfile.filename, file_path=temp_file.name) -def entrypoint(func: Callable[..., Any]) -> Callable[..., Any]: +def entrypoint(func: Callable[..., T]) -> Callable[..., Dict[str, T]]: """ Decorator to wrap a function for HTTP POST and terminal exposure. @@ -68,14 +71,14 @@ def entrypoint(func: Callable[..., Any]) -> Callable[..., Any]: ingestible_files = extract_ingestible_files(func_signature) @functools.wraps(func) - def wrapper(*args, **kwargs) -> Any: + def wrapper(*args, **kwargs) -> Dict[str, T]: func_params, api_config_params = split_kwargs(kwargs, config_params) ingest_files(func_params, ingestible_files) agenta.config.set(**api_config_params) return execute_function(func, *args, **func_params) @functools.wraps(func) - def wrapper_deployed(*args, **kwargs) -> Any: + def wrapper_deployed(*args, **kwargs) -> FuncResponse: func_params = { k: v for k, v in kwargs.items() if k not in ["config", "environment"] } @@ -89,7 +92,7 @@ def wrapper_deployed(*args, **kwargs) -> Any: update_function_signature(wrapper, func_signature, config_params, ingestible_files) route = f"/{endpoint_name}" - app.post(route)(wrapper) + app.post(route, response_model=FuncResponse)(wrapper) update_deployed_function_signature( wrapper_deployed, @@ -97,7 +100,7 @@ def wrapper_deployed(*args, **kwargs) -> Any: ingestible_files, ) route_deployed = f"/{endpoint_name}_deployed" - app.post(route_deployed)(wrapper_deployed) + app.post(route_deployed, response_model=FuncResponse)(wrapper_deployed) override_schema( openapi_schema=app.openapi(), func_name=func.__name__, @@ -142,13 +145,33 @@ def ingest_files( func_params[name] = ingest_file(func_params[name]) -def execute_function(func: Callable[..., Any], *args, **func_params) -> Any: - """Execute the function and handle any exceptions.""" +def execute_function( + func: Callable[..., Any], *args, **func_params +) -> Union[Dict[str, Any], JSONResponse]: + """ + Execute the given function and handle any exceptions. + + Parameters: + - func: The function to be executed. + - args: Positional arguments for the function. + - func_params: Keyword arguments for the function. + + Returns: + Either a dictionary or a JSONResponse object. + """ + try: + start_time = time.time() result = func(*args, **func_params) + end_time = time.time() + latency = end_time - start_time + if isinstance(result, Context): save_context(result) - return result + if isinstance(result, FuncResponse): + return FuncResponse(**result, latency=str(latency)).dict() + if isinstance(result, str): + return FuncResponse(message=result, latency=str(latency)).dict() except Exception as e: return handle_exception(e) From 993a45596afa835f34f374023f3fbfa440b3facb Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 14 Dec 2023 11:02:02 +0100 Subject: [PATCH 003/267] Update - modified execute_function to make use of time.perf_counter and also cleanup imports and entrypoint function --- agenta-cli/agenta/sdk/agenta_decorator.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/agenta-cli/agenta/sdk/agenta_decorator.py b/agenta-cli/agenta/sdk/agenta_decorator.py index 9a34ccaaa1..58646b2e21 100644 --- a/agenta-cli/agenta/sdk/agenta_decorator.py +++ b/agenta-cli/agenta/sdk/agenta_decorator.py @@ -8,7 +8,7 @@ import functools from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Any, Callable, Dict, Optional, Tuple, List, TypeVar +from typing import Any, Callable, Dict, Optional, Tuple, List, Union from fastapi import Body, FastAPI, UploadFile from fastapi.responses import JSONResponse @@ -31,7 +31,6 @@ ) app = FastAPI() -T = TypeVar("T") origins = [ "*", @@ -55,7 +54,7 @@ def ingest_file(upfile: UploadFile): return InFile(file_name=upfile.filename, file_path=temp_file.name) -def entrypoint(func: Callable[..., T]) -> Callable[..., Dict[str, T]]: +def entrypoint(func: Callable[..., Any]) -> Callable[..., Any]: """ Decorator to wrap a function for HTTP POST and terminal exposure. @@ -151,7 +150,7 @@ def ingest_files( func_params[name] = ingest_file(func_params[name]) -async def execute_function(func: Callable[..., Any], *args, **func_params) -> Any: +async def execute_function(func: Callable[..., Any], *args, **func_params) -> Union[Dict[str, Any], JSONResponse]: """Execute the function and handle any exceptions.""" try: From ecad1c206c357edb8d3fa7dedb0b657f66a07d70 Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 14 Dec 2023 11:03:14 +0100 Subject: [PATCH 004/267] :art: Format - ran black --- agenta-cli/agenta/sdk/agenta_decorator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agenta-cli/agenta/sdk/agenta_decorator.py b/agenta-cli/agenta/sdk/agenta_decorator.py index 58646b2e21..08a960dab8 100644 --- a/agenta-cli/agenta/sdk/agenta_decorator.py +++ b/agenta-cli/agenta/sdk/agenta_decorator.py @@ -150,7 +150,9 @@ def ingest_files( func_params[name] = ingest_file(func_params[name]) -async def execute_function(func: Callable[..., Any], *args, **func_params) -> Union[Dict[str, Any], JSONResponse]: +async def execute_function( + func: Callable[..., Any], *args, **func_params +) -> Union[Dict[str, Any], JSONResponse]: """Execute the function and handle any exceptions.""" try: From c686dfe2eedb57dbbbc8535d154e4b68448346a9 Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 14 Dec 2023 18:23:47 +0100 Subject: [PATCH 005/267] Update - refactor types for func execute in sdk --- agenta-cli/agenta/sdk/types.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/agenta-cli/agenta/sdk/types.py b/agenta-cli/agenta/sdk/types.py index a18680ecb9..2aa4315a69 100644 --- a/agenta-cli/agenta/sdk/types.py +++ b/agenta-cli/agenta/sdk/types.py @@ -10,17 +10,17 @@ def __init__(self, file_name: str, file_path: str): self.file_path = file_path -class FuncTokenUsage(BaseModel): - completion_tokens: str - prompt_tokens: str - total_tokens: str +class LLMTokenUsage(BaseModel): + completion_tokens: int + prompt_tokens: int + total_tokens: int class FuncResponse(BaseModel): message: str - usage: Optional[FuncTokenUsage] + usage: Optional[LLMTokenUsage] cost: Optional[str] - latency: str + latency: float class DictInput(dict): From c9a0cbc100ceac84715ccb53d058187f6825cc6f Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 14 Dec 2023 18:24:34 +0100 Subject: [PATCH 006/267] Update - modified execute_function return response --- agenta-cli/agenta/sdk/agenta_decorator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agenta-cli/agenta/sdk/agenta_decorator.py b/agenta-cli/agenta/sdk/agenta_decorator.py index 08a960dab8..08de0d664f 100644 --- a/agenta-cli/agenta/sdk/agenta_decorator.py +++ b/agenta-cli/agenta/sdk/agenta_decorator.py @@ -175,10 +175,10 @@ async def execute_function( if isinstance(result, Context): save_context(result) - if isinstance(result, FuncResponse): - return FuncResponse(**result, latency=str(latency)).dict() + if isinstance(result, Dict): + return FuncResponse(**result, latency=round(latency, 4)).dict() if isinstance(result, str): - return FuncResponse(message=result, latency=str(latency)).dict() + return FuncResponse(message=result, latency=round(latency, 4)).dict() except Exception as e: return handle_exception(e) From 33c43e616d729478550c4318ae1ebc156fcc8a3f Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 14 Dec 2023 18:27:29 +0100 Subject: [PATCH 007/267] Feat - created example app that uses the new sdk output format --- examples/async_chat_sdk_output_format/app.py | 40 +++++++++++++++++++ .../requirements.txt | 2 + 2 files changed, 42 insertions(+) create mode 100644 examples/async_chat_sdk_output_format/app.py create mode 100644 examples/async_chat_sdk_output_format/requirements.txt diff --git a/examples/async_chat_sdk_output_format/app.py b/examples/async_chat_sdk_output_format/app.py new file mode 100644 index 0000000000..4f52722168 --- /dev/null +++ b/examples/async_chat_sdk_output_format/app.py @@ -0,0 +1,40 @@ +import agenta as ag +from agenta import FloatParam, MessagesInput, MultipleChoiceParam +from openai import AsyncOpenAI + + +client = AsyncOpenAI() + +SYSTEM_PROMPT = "You have expertise in offering technical ideas to startups." +CHAT_LLM_GPT = [ + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-16k-0613", + "gpt-4", +] + +ag.init() +ag.config.default( + temperature=FloatParam(0.2), + model=MultipleChoiceParam("gpt-3.5-turbo", CHAT_LLM_GPT), + max_tokens=ag.IntParam(-1, -1, 4000), + prompt_system=ag.TextParam(SYSTEM_PROMPT), +) + + +@ag.entrypoint +async def chat(inputs: MessagesInput = MessagesInput()) -> str: + messages = [{"role": "system", "content": ag.config.prompt_system}] + inputs + max_tokens = ag.config.max_tokens if ag.config.max_tokens != -1 else None + chat_completion = await client.chat.completions.create( + model=ag.config.model, + messages=messages, + temperature=ag.config.temperature, + max_tokens=max_tokens, + ) + return { + "message": chat_completion.choices[0].message.content, + **{"usage": chat_completion.usage.dict()} + # "cost": ... + } diff --git a/examples/async_chat_sdk_output_format/requirements.txt b/examples/async_chat_sdk_output_format/requirements.txt new file mode 100644 index 0000000000..310f162cec --- /dev/null +++ b/examples/async_chat_sdk_output_format/requirements.txt @@ -0,0 +1,2 @@ +agenta +openai \ No newline at end of file From df2dd20d32c9c4d1b85edfefa47c06f0178339de Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 18 Dec 2023 19:08:53 +0100 Subject: [PATCH 008/267] Minor refactor --- agenta-cli/agenta/sdk/agenta_decorator.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/agenta-cli/agenta/sdk/agenta_decorator.py b/agenta-cli/agenta/sdk/agenta_decorator.py index 08de0d664f..6a11594984 100644 --- a/agenta-cli/agenta/sdk/agenta_decorator.py +++ b/agenta-cli/agenta/sdk/agenta_decorator.py @@ -162,16 +162,13 @@ async def execute_function( it awaits their execution. """ is_coroutine_function = inspect.iscoroutinefunction(func) + start_time = time.perf_counter() if is_coroutine_function: - start_time = time.perf_counter() result = await func(*args, **func_params) - end_time = time.perf_counter() - latency = end_time - start_time else: - start_time = time.perf_counter() result = func(*args, **func_params) - end_time = time.perf_counter() - latency = end_time - start_time + end_time = time.perf_counter() + latency = end_time - start_time if isinstance(result, Context): save_context(result) From 60f483f7b0e8e654ee44b06cf37b79677ea6fb07 Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 19 Dec 2023 08:02:46 +0100 Subject: [PATCH 009/267] Feat - created openai_cost helper function --- .../agenta/sdk/utils/helper/openai_cost.py | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 agenta-cli/agenta/sdk/utils/helper/openai_cost.py diff --git a/agenta-cli/agenta/sdk/utils/helper/openai_cost.py b/agenta-cli/agenta/sdk/utils/helper/openai_cost.py new file mode 100644 index 0000000000..393418eaf5 --- /dev/null +++ b/agenta-cli/agenta/sdk/utils/helper/openai_cost.py @@ -0,0 +1,139 @@ +MODEL_COST_PER_1K_TOKENS = { + # GPT-4 input + "gpt-4": 0.03, + "gpt-4-0314": 0.03, + "gpt-4-0613": 0.03, + "gpt-4-32k": 0.06, + "gpt-4-32k-0314": 0.06, + "gpt-4-32k-0613": 0.06, + "gpt-4-vision-preview": 0.01, + "gpt-4-1106-preview": 0.01, + # GPT-4 output + "gpt-4-completion": 0.06, + "gpt-4-0314-completion": 0.06, + "gpt-4-0613-completion": 0.06, + "gpt-4-32k-completion": 0.12, + "gpt-4-32k-0314-completion": 0.12, + "gpt-4-32k-0613-completion": 0.12, + "gpt-4-vision-preview-completion": 0.03, + "gpt-4-1106-preview-completion": 0.03, + # GPT-3.5 input + "gpt-3.5-turbo": 0.0015, + "gpt-3.5-turbo-0301": 0.0015, + "gpt-3.5-turbo-0613": 0.0015, + "gpt-3.5-turbo-1106": 0.001, + "gpt-3.5-turbo-instruct": 0.0015, + "gpt-3.5-turbo-16k": 0.003, + "gpt-3.5-turbo-16k-0613": 0.003, + # GPT-3.5 output + "gpt-3.5-turbo-completion": 0.002, + "gpt-3.5-turbo-0301-completion": 0.002, + "gpt-3.5-turbo-0613-completion": 0.002, + "gpt-3.5-turbo-1106-completion": 0.002, + "gpt-3.5-turbo-instruct-completion": 0.002, + "gpt-3.5-turbo-16k-completion": 0.004, + "gpt-3.5-turbo-16k-0613-completion": 0.004, + # Azure GPT-35 input + "gpt-35-turbo": 0.0015, # Azure OpenAI version of ChatGPT + "gpt-35-turbo-0301": 0.0015, # Azure OpenAI version of ChatGPT + "gpt-35-turbo-0613": 0.0015, + "gpt-35-turbo-instruct": 0.0015, + "gpt-35-turbo-16k": 0.003, + "gpt-35-turbo-16k-0613": 0.003, + # Azure GPT-35 output + "gpt-35-turbo-completion": 0.002, # Azure OpenAI version of ChatGPT + "gpt-35-turbo-0301-completion": 0.002, # Azure OpenAI version of ChatGPT + "gpt-35-turbo-0613-completion": 0.002, + "gpt-35-turbo-instruct-completion": 0.002, + "gpt-35-turbo-16k-completion": 0.004, + "gpt-35-turbo-16k-0613-completion": 0.004, + # Others + "text-ada-001": 0.0004, + "ada": 0.0004, + "text-babbage-001": 0.0005, + "babbage": 0.0005, + "text-curie-001": 0.002, + "curie": 0.002, + "text-davinci-003": 0.02, + "text-davinci-002": 0.02, + "code-davinci-002": 0.02, + # Fine Tuned input + "babbage-002-finetuned": 0.0016, + "davinci-002-finetuned": 0.012, + "gpt-3.5-turbo-0613-finetuned": 0.012, + # Fine Tuned output + "babbage-002-finetuned-completion": 0.0016, + "davinci-002-finetuned-completion": 0.012, + "gpt-3.5-turbo-0613-finetuned-completion": 0.016, + # Azure Fine Tuned input + "babbage-002-azure-finetuned": 0.0004, + "davinci-002-azure-finetuned": 0.002, + "gpt-35-turbo-0613-azure-finetuned": 0.0015, + # Azure Fine Tuned output + "babbage-002-azure-finetuned-completion": 0.0004, + "davinci-002-azure-finetuned-completion": 0.002, + "gpt-35-turbo-0613-azure-finetuned-completion": 0.002, + # Legacy fine-tuned models + "ada-finetuned-legacy": 0.0016, + "babbage-finetuned-legacy": 0.0024, + "curie-finetuned-legacy": 0.012, + "davinci-finetuned-legacy": 0.12, +} + + +def standardize_model_name( + model_name: str, + is_completion: bool = False, +) -> str: + """ + Standardize the model name to a format that can be used in the OpenAI API. + + Args: + model_name: Model name to standardize. + is_completion: Whether the model is used for completion or not. + Defaults to False. + + Returns: + Standardized model name. + + """ + model_name = model_name.lower() + if ".ft-" in model_name: + model_name = model_name.split(".ft-")[0] + "-azure-finetuned" + if ":ft-" in model_name: + model_name = model_name.split(":")[0] + "-finetuned-legacy" + if "ft:" in model_name: + model_name = model_name.split(":")[1] + "-finetuned" + if is_completion and ( + model_name.startswith("gpt-4") + or model_name.startswith("gpt-3.5") + or model_name.startswith("gpt-35") + or ("finetuned" in model_name and "legacy" not in model_name) + ): + return model_name + "-completion" + else: + return model_name + + +def get_openai_token_cost_for_model( + model_name: str, num_tokens: int, is_completion: bool = False +) -> float: + """ + Get the cost in USD for a given model and number of tokens. + + Args: + model_name: Name of the model + num_tokens: Number of tokens. + is_completion: Whether the model is used for completion or not. + Defaults to False. + + Returns: + Cost in USD. + """ + model_name = standardize_model_name(model_name, is_completion=is_completion) + if model_name not in MODEL_COST_PER_1K_TOKENS: + raise ValueError( + f"Unknown model: {model_name}. Please provide a valid OpenAI model name." + "Known models are: " + ", ".join(MODEL_COST_PER_1K_TOKENS.keys()) + ) + return MODEL_COST_PER_1K_TOKENS[model_name] * (num_tokens / 1000) From 99a95e93e138c927b8020e1bb80467d1c79cde34 Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 19 Dec 2023 08:34:02 +0100 Subject: [PATCH 010/267] Update - change cost type to float from str --- agenta-cli/agenta/sdk/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/agenta-cli/agenta/sdk/__init__.py b/agenta-cli/agenta/sdk/__init__.py index b10b8c1e17..6146aec971 100644 --- a/agenta-cli/agenta/sdk/__init__.py +++ b/agenta-cli/agenta/sdk/__init__.py @@ -14,5 +14,6 @@ FileInputURL, ) from .agenta_init import Config, init +from .utils.helper.openai_cost import get_openai_token_cost_for_model config = PreInitObject("agenta.config", Config) From 2a1f3813ebdd541ef91ad0322f4991140114c34c Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 19 Dec 2023 08:34:58 +0100 Subject: [PATCH 011/267] Feat - implemented calculate_token_usage helper function --- .../agenta/sdk/utils/helper/openai_cost.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/agenta-cli/agenta/sdk/utils/helper/openai_cost.py b/agenta-cli/agenta/sdk/utils/helper/openai_cost.py index 393418eaf5..e4d59fa56a 100644 --- a/agenta-cli/agenta/sdk/utils/helper/openai_cost.py +++ b/agenta-cli/agenta/sdk/utils/helper/openai_cost.py @@ -95,8 +95,8 @@ def standardize_model_name( Returns: Standardized model name. - """ + model_name = model_name.lower() if ".ft-" in model_name: model_name = model_name.split(".ft-")[0] + "-azure-finetuned" @@ -130,6 +130,7 @@ def get_openai_token_cost_for_model( Returns: Cost in USD. """ + model_name = standardize_model_name(model_name, is_completion=is_completion) if model_name not in MODEL_COST_PER_1K_TOKENS: raise ValueError( @@ -137,3 +138,28 @@ def get_openai_token_cost_for_model( "Known models are: " + ", ".join(MODEL_COST_PER_1K_TOKENS.keys()) ) return MODEL_COST_PER_1K_TOKENS[model_name] * (num_tokens / 1000) + + +def calculate_token_usage(model_name: str, token_usage: dict) -> float: + """Calculates the total cost of using a language model based on the model name and token + usage. + + Args: + model_name: The name of the model used to determine the cost per token. + token_usage: Contains information about the usage of tokens for a particular model. + + Returns: + Total cost of using a model. + """ + + completion_tokens = token_usage.get("completion_tokens", 0) + prompt_tokens = token_usage.get("prompt_tokens", 0) + model_name = standardize_model_name(model_name) + if model_name in MODEL_COST_PER_1K_TOKENS: + completion_cost = get_openai_token_cost_for_model( + model_name, completion_tokens, is_completion=True + ) + prompt_cost = get_openai_token_cost_for_model(model_name, prompt_tokens) + total_cost = prompt_cost + completion_cost + return total_cost + return 0 \ No newline at end of file From 590ad2d165459d8e330747a60dde4bb330fb6231 Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 19 Dec 2023 08:35:12 +0100 Subject: [PATCH 012/267] Update - change cost type to float from str --- agenta-cli/agenta/sdk/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-cli/agenta/sdk/types.py b/agenta-cli/agenta/sdk/types.py index 2aa4315a69..419de750e9 100644 --- a/agenta-cli/agenta/sdk/types.py +++ b/agenta-cli/agenta/sdk/types.py @@ -19,7 +19,7 @@ class LLMTokenUsage(BaseModel): class FuncResponse(BaseModel): message: str usage: Optional[LLMTokenUsage] - cost: Optional[str] + cost: Optional[float] latency: float From f22104b6b6e90c7aa038f2c67912b30413cf57ce Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 19 Dec 2023 08:35:48 +0100 Subject: [PATCH 013/267] Update - import calculate_token_usage helper function in sdk/__init__.py module --- agenta-cli/agenta/__init__.py | 1 + agenta-cli/agenta/sdk/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/agenta-cli/agenta/__init__.py b/agenta-cli/agenta/__init__.py index b73eb24d60..e8439a530c 100644 --- a/agenta-cli/agenta/__init__.py +++ b/agenta-cli/agenta/__init__.py @@ -13,5 +13,6 @@ ) from .sdk.utils.preinit import PreInitObject from .sdk.agenta_init import Config, init +from .sdk.utils.helper.openai_cost import calculate_token_usage config = PreInitObject("agenta.config", Config) diff --git a/agenta-cli/agenta/sdk/__init__.py b/agenta-cli/agenta/sdk/__init__.py index 6146aec971..fcc05b4d7a 100644 --- a/agenta-cli/agenta/sdk/__init__.py +++ b/agenta-cli/agenta/sdk/__init__.py @@ -14,6 +14,6 @@ FileInputURL, ) from .agenta_init import Config, init -from .utils.helper.openai_cost import get_openai_token_cost_for_model +from .utils.helper.openai_cost import calculate_token_usage config = PreInitObject("agenta.config", Config) From 40734db54d3a6bff8371eaed319623333d3a191d Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 19 Dec 2023 08:37:32 +0100 Subject: [PATCH 014/267] Update - added cost to example app --- examples/async_chat_sdk_output_format/app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/async_chat_sdk_output_format/app.py b/examples/async_chat_sdk_output_format/app.py index 4f52722168..8a5cad8f36 100644 --- a/examples/async_chat_sdk_output_format/app.py +++ b/examples/async_chat_sdk_output_format/app.py @@ -24,7 +24,7 @@ @ag.entrypoint -async def chat(inputs: MessagesInput = MessagesInput()) -> str: +async def chat(inputs: MessagesInput = MessagesInput()): messages = [{"role": "system", "content": ag.config.prompt_system}] + inputs max_tokens = ag.config.max_tokens if ag.config.max_tokens != -1 else None chat_completion = await client.chat.completions.create( @@ -33,8 +33,9 @@ async def chat(inputs: MessagesInput = MessagesInput()) -> str: temperature=ag.config.temperature, max_tokens=max_tokens, ) + token_usage = chat_completion.usage.dict() return { "message": chat_completion.choices[0].message.content, - **{"usage": chat_completion.usage.dict()} - # "cost": ... + **{"usage": token_usage}, + "cost": ag.calculate_token_usage(ag.config.model, token_usage) } From 0e32ea57bb8efde038a39877109edc3755353854 Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 19 Dec 2023 08:37:52 +0100 Subject: [PATCH 015/267] :art: Format - ran black --- agenta-cli/agenta/sdk/utils/helper/openai_cost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-cli/agenta/sdk/utils/helper/openai_cost.py b/agenta-cli/agenta/sdk/utils/helper/openai_cost.py index e4d59fa56a..2d436a4acd 100644 --- a/agenta-cli/agenta/sdk/utils/helper/openai_cost.py +++ b/agenta-cli/agenta/sdk/utils/helper/openai_cost.py @@ -162,4 +162,4 @@ def calculate_token_usage(model_name: str, token_usage: dict) -> float: prompt_cost = get_openai_token_cost_for_model(model_name, prompt_tokens) total_cost = prompt_cost + completion_cost return total_cost - return 0 \ No newline at end of file + return 0 From 72366c11cc12ab631ae3c91b0f3c694b31f6f766 Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 19 Dec 2023 08:39:12 +0100 Subject: [PATCH 016/267] :art: Format - ran black --- examples/async_chat_sdk_output_format/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/async_chat_sdk_output_format/app.py b/examples/async_chat_sdk_output_format/app.py index 8a5cad8f36..7eb743861d 100644 --- a/examples/async_chat_sdk_output_format/app.py +++ b/examples/async_chat_sdk_output_format/app.py @@ -37,5 +37,5 @@ async def chat(inputs: MessagesInput = MessagesInput()): return { "message": chat_completion.choices[0].message.content, **{"usage": token_usage}, - "cost": ag.calculate_token_usage(ag.config.model, token_usage) + "cost": ag.calculate_token_usage(ag.config.model, token_usage), } From 6b10ce350c1d39c6d13a2ea6b17b96a8b151d81c Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Thu, 21 Dec 2023 20:08:46 +0100 Subject: [PATCH 017/267] add costs source --- agenta-cli/agenta/sdk/utils/helper/openai_cost.py | 1 + 1 file changed, 1 insertion(+) diff --git a/agenta-cli/agenta/sdk/utils/helper/openai_cost.py b/agenta-cli/agenta/sdk/utils/helper/openai_cost.py index 2d436a4acd..8473dee57f 100644 --- a/agenta-cli/agenta/sdk/utils/helper/openai_cost.py +++ b/agenta-cli/agenta/sdk/utils/helper/openai_cost.py @@ -1,3 +1,4 @@ +# https://raw.githubusercontent.com/langchain-ai/langchain/23eb480c3866db8693a3a2d63b787c898c54bb35/libs/community/langchain_community/callbacks/openai_info.py MODEL_COST_PER_1K_TOKENS = { # GPT-4 input "gpt-4": 0.03, From b5d4cdb5a9d18933944a3dbf507226029574d291 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Fri, 22 Dec 2023 15:12:12 +0100 Subject: [PATCH 018/267] feat: add cost, latency and usage to playground --- .../components/Playground/Views/TestView.tsx | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/agenta-web/src/components/Playground/Views/TestView.tsx b/agenta-web/src/components/Playground/Views/TestView.tsx index fd542a6de2..96382fc44f 100644 --- a/agenta-web/src/components/Playground/Views/TestView.tsx +++ b/agenta-web/src/components/Playground/Views/TestView.tsx @@ -94,6 +94,11 @@ interface BoxComponentProps { inputParams: Parameter[] | null testData: GenericObject result: string + additionalData: { + cost: number | null + latency: number | null + usage: {completion_tokens: number; prompt_tokens: number; total_tokens: number} | null + } onInputParamChange: (paramName: string, newValue: any) => void onRun: () => void onAddToTestset: (params: Record) => void @@ -105,6 +110,7 @@ const BoxComponent: React.FC = ({ inputParams, testData, result, + additionalData, onInputParamChange, onRun, onAddToTestset, @@ -155,6 +161,28 @@ const BoxComponent: React.FC = ({ imageSize="large" /> + {additionalData && ( + +

+ Tokens:{" "} + {additionalData.usage !== null + ? JSON.stringify(additionalData.usage.total_tokens) + : 0} +

+

+ Cost:{" "} + {additionalData.cost !== null + ? `$${additionalData.cost.toFixed(4)}` + : "$0.00"} +

+

+ Latency:{" "} + {additionalData.latency !== null + ? `${Math.round(additionalData.latency * 1000)}ms` + : "0ms"} +

+
+ )} + ), }, From 5b7fb3a5df47a455540a7356926f2e070883007a Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 19 Dec 2023 13:20:17 +0100 Subject: [PATCH 094/267] Update pyproject.toml --- agenta-cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-cli/pyproject.toml b/agenta-cli/pyproject.toml index 7b5762ab0e..107fe27e4d 100644 --- a/agenta-cli/pyproject.toml +++ b/agenta-cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "agenta" -version = "0.6.5" +version = "0.6.6" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" authors = ["Mahmoud Mabrouk "] From 30499ee3210f449ec51b22227a2a115728a17e2a Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 19 Dec 2023 13:49:01 +0100 Subject: [PATCH 095/267] Add changelog and update navigation in mint.json --- docs/changelog/main.mdx | 74 +++++++++++++++++++++++++++++++++++++++++ docs/mint.json | 14 ++++++++ 2 files changed, 88 insertions(+) create mode 100644 docs/changelog/main.mdx diff --git a/docs/changelog/main.mdx b/docs/changelog/main.mdx new file mode 100644 index 0000000000..f0da6c86a4 --- /dev/null +++ b/docs/changelog/main.mdx @@ -0,0 +1,74 @@ +--- +title: "Changelog" +--- + +## v0.6.6 - Improving Side-by-side Comparison in the Playground +*19th December 2023* +- Enhanced the side-by-side comparison in the playground for better user experience + +## v0.6.5 - Resolved Batch Logic Issue in Evaluation +*18th December 2023* +- Resolved an issue with batch logic in evaluation (users can now run extensive evaluations) + +## v0.6.4 - Comprehensive Updates and Bug Fixes +*12th December 2023* +- Incorporated all chat turns to the chat set +- Rectified self-hosting documentation +- Introduced asynchronous support for applications +- Added 'register_default' alias +- Fixed a bug in the side-by-side feature + +## v0.6.3 - Integrated File Input and UI Enhancements +*12th December 2023* +- Integrated file input feature in the SDK +- Provided an example that includes images +- Upgraded the human evaluation view to present larger inputs +- Fixed issues related to data overwriting in the cloud +- Implemented UI enhancements to the side bar + +## v0.6.2 - Minor Adjustments for Better Performance +*7th December 2023* +- Made minor adjustments + +## v0.6.1 - Bug Fix for Application Saving +*7th December 2023* +- Resolved a bug related to saving the application + +## v0.6.0 - Introduction of Chat-based Applications +*1st December 2023* +- Introduced chat-based applications +- Fixed a bug in 'export csv' feature in auto evaluation + +## v0.5.8 - Multiple UI and CSV Reader Fixes +*1st December 2023* +- Fixed a bug impacting the csv reader +- Addressed an issue of variant overwriting +- Made tabs draggable for better UI navigation +- Implemented support for multiple LLM keys in the UI + +## v0.5.7 - Enhanced Self-hosting and Mistral Model Tutorial +*17th November 2023* +- Enhanced and simplified self-hosting feature +- Added a tutorial for the Mistral model +- Resolved a race condition issue in deployment +- Fixed an issue with saving in the playground + +## v0.5.6 - Sentry Integration and User Communication Improvements +*12th November 2023* +- Enhanced bug tracking with Sentry integration in the cloud +- Integrated Intercom for better user communication in the cloud +- Upgraded to the latest version of OpenAI +- Cleaned up files post serving in CLI + +## v0.5.5 - Cypress Tests and UI Improvements +*2nd November 2023* +- Conducted extensive Cypress tests for improved application stability +- Added a collapsible sidebar for better navigation +- Improved error handling mechanisms +- Added documentation for the evaluation feature + +## v0.5 - Launch of SDK Version 2 and Cloud-hosted Version +*23rd October 2023* +- Launched SDK version 2 +- Launched the cloud-hosted version +- Completed a comprehensive refactoring of the application diff --git a/docs/mint.json b/docs/mint.json index b6c2ff4b35..10bff3da78 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -49,9 +49,17 @@ } ], "tabs": [ + { + "name": "Cookbook", + "url": "cookbook" + }, { "name": "Reference", "url": "reference" + }, + { + "name": "Changelog", + "url": "changelog" } ], "navigation": [ @@ -217,6 +225,12 @@ ] } ] + }, + { + "group": "Changelog", + "pages": [ + "changelog/main" + ] } ], "api": { From a49e91d32bce01a528de076af649134812999ff5 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 19 Dec 2023 13:54:03 +0100 Subject: [PATCH 096/267] Update file paths in links --- docs/contributing/getting-started.mdx | 2 +- docs/quickstart/getting-started-ui.mdx | 2 +- docs/quickstart/installation.mdx | 4 ++-- docs/quickstart/introduction.mdx | 6 +++--- docs/self-host/host-locally.mdx | 2 +- docs/tutorials/a-more-complicated-tutorial-draft.mdx | 4 ++-- docs/tutorials/first-app-with-langchain.mdx | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/contributing/getting-started.mdx b/docs/contributing/getting-started.mdx index a95454765b..4c4425f7be 100644 --- a/docs/contributing/getting-started.mdx +++ b/docs/contributing/getting-started.mdx @@ -28,7 +28,7 @@ To maintain code quality, we adhere to certain formatting and linting rules: ## Contribution Steps -1. **Pick an Issue:** Start by selecting an issue from our issue tracker. Choose one that matches your skill set and begin coding. For more on this, read our [Creating an Issue Guide](file-issue). +1. **Pick an Issue:** Start by selecting an issue from our issue tracker. Choose one that matches your skill set and begin coding. For more on this, read our [Creating an Issue Guide](/contributing/file-issue). 2. **Fork & Pull Request:** Fork our repository, create a new branch, add your changes, and submit a pull request. Ensure your code aligns with our standards and includes appropriate unit tests. diff --git a/docs/quickstart/getting-started-ui.mdx b/docs/quickstart/getting-started-ui.mdx index abd83629c2..d2363ce261 100644 --- a/docs/quickstart/getting-started-ui.mdx +++ b/docs/quickstart/getting-started-ui.mdx @@ -57,4 +57,4 @@ You can now find the API endpoint in the "Endpoints" menu. Copy and paste the co - Congratulations! You've created your first LLM application. Feel free to modify it, explore its parameters, and discover Agenta's features. Your next steps could include [building an application using your own code](getting-started-code.mdx), or following one of our UI-based tutorials. + Congratulations! You've created your first LLM application. Feel free to modify it, explore its parameters, and discover Agenta's features. Your next steps could include [building an application using your own code](/quickstart/getting-started-code.mdx), or following one of our UI-based tutorials. diff --git a/docs/quickstart/installation.mdx b/docs/quickstart/installation.mdx index 0b700af772..4f23d5b401 100644 --- a/docs/quickstart/installation.mdx +++ b/docs/quickstart/installation.mdx @@ -3,7 +3,7 @@ title: Installation description: 'How to install the Agenta CLI on your machine' --- - This guide helps you install Agenta on your local machine. If you're looking to set it up on a server for multiple users, head over to [this guide](/installation/self-hosting/). + This guide helps you install Agenta on your local machine. If you're looking to set it up on a server for multiple users, head over to [this guide](/self-host/host-remotely). # Installing Agenta locally To install Agenta, you need first to install the python package containing both the SDK and the CLI. Then, you need to install the web platform. @@ -57,6 +57,6 @@ Open your browser and go to [http://localhost](http://localhost). If you see the ## What's next? You're all set to start using Agenta! - + Click here to build your first LLM app in just 1 minute. \ No newline at end of file diff --git a/docs/quickstart/introduction.mdx b/docs/quickstart/introduction.mdx index cc762fc16b..407f847795 100644 --- a/docs/quickstart/introduction.mdx +++ b/docs/quickstart/introduction.mdx @@ -27,7 +27,7 @@ Agenta integrates with all frameworks and model providers in the ecosystem, such Learn the main concepts behind agenta. @@ -35,7 +35,7 @@ Agenta integrates with all frameworks and model providers in the ecosystem, such Create and deploy your first app from the UI in under 2 minutes. @@ -44,7 +44,7 @@ Agenta integrates with all frameworks and model providers in the ecosystem, such title="Get Started from code" icon="code" color="#337BFF" - href="getting-started-code"> + href="/quickstart/getting-started-code"> Write a custom LLM app and evaluate it in 10 minutes. diff --git a/docs/self-host/host-locally.mdx b/docs/self-host/host-locally.mdx index d48f599bfc..798c7848ec 100644 --- a/docs/self-host/host-locally.mdx +++ b/docs/self-host/host-locally.mdx @@ -51,6 +51,6 @@ Open your browser and go to [http://localhost](http://localhost). If you see the ## What's next? You're all set to start using Agenta! - + Click here to build your first LLM app in just 1 minute. diff --git a/docs/tutorials/a-more-complicated-tutorial-draft.mdx b/docs/tutorials/a-more-complicated-tutorial-draft.mdx index 98dc178aac..9fb087365c 100644 --- a/docs/tutorials/a-more-complicated-tutorial-draft.mdx +++ b/docs/tutorials/a-more-complicated-tutorial-draft.mdx @@ -7,9 +7,9 @@ In this tutorial, we'll lead you through the process of creating your first Lang Let's begin. -## Prerequisites +## Installation -This guide assumes you have completed the installation process. If not, please follow our [installation guide](/installation). +Run `pip install agenta` to install the Agenta CLI. ## 1. Project Initialization diff --git a/docs/tutorials/first-app-with-langchain.mdx b/docs/tutorials/first-app-with-langchain.mdx index 4aea792e9d..8100c77649 100644 --- a/docs/tutorials/first-app-with-langchain.mdx +++ b/docs/tutorials/first-app-with-langchain.mdx @@ -4,9 +4,9 @@ title: Simple App with Langchain This tutorial guides you through writing of your first LLM app using Langchain and Agenta. The objective is to create an app that can produce a persuasive startup pitch, using the startup's name and core idea. By the end of this tutorial, your app will be set up locally and ready for testing and refinement in the playground. -## Prerequisites +## Installation -This guide assumes you have completed the installation process. If not, please follow our [installation guide](/installation). +Run `pip install agenta` to install the Agenta SDK. ## 1. Project Initialization From a9678b6c584741b7b2059f548a53f38fbadf8c36 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 13 Dec 2023 15:57:16 +0100 Subject: [PATCH 097/267] Feat - implemented BinaryParam SDK type and BoolMeta class --- agenta-cli/agenta/__init__.py | 1 + agenta-cli/agenta/sdk/__init__.py | 1 + agenta-cli/agenta/sdk/types.py | 35 ++++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/agenta-cli/agenta/__init__.py b/agenta-cli/agenta/__init__.py index e8439a530c..0688097e82 100644 --- a/agenta-cli/agenta/__init__.py +++ b/agenta-cli/agenta/__init__.py @@ -10,6 +10,7 @@ MessagesInput, TextParam, FileInputURL, + BinaryParam ) from .sdk.utils.preinit import PreInitObject from .sdk.agenta_init import Config, init diff --git a/agenta-cli/agenta/sdk/__init__.py b/agenta-cli/agenta/sdk/__init__.py index fcc05b4d7a..cdea47f0f7 100644 --- a/agenta-cli/agenta/sdk/__init__.py +++ b/agenta-cli/agenta/sdk/__init__.py @@ -12,6 +12,7 @@ TextParam, MessagesInput, FileInputURL, + BinaryParam ) from .agenta_init import Config, init from .utils.helper.openai_cost import calculate_token_usage diff --git a/agenta-cli/agenta/sdk/types.py b/agenta-cli/agenta/sdk/types.py index 419de750e9..0edab46eb1 100644 --- a/agenta-cli/agenta/sdk/types.py +++ b/agenta-cli/agenta/sdk/types.py @@ -1,7 +1,7 @@ import json from typing import Any, Dict, List, Optional -from pydantic import BaseModel, Extra, HttpUrl +from pydantic import BaseModel, Extra, HttpUrl, Field class InFile: @@ -42,6 +42,39 @@ def __modify_schema__(cls, field_schema): field_schema.update({"x-parameter": "text"}) +class BinaryParamMixin(BaseModel): + default: bool + + @property + def type(self) -> bool: + return "bool" + + +class BoolMeta(type): + """ + This meta class handles the behavior of a boolean without + directly inheriting from it (avoiding the conflict + that comes from inheriting bool). + """ + + def __new__(cls, name: str, bases: tuple, namespace: dict): + if "default" in namespace and namespace["default"] not in [0, 1]: + raise ValueError("Must provide either 0 or 1") + namespace["default"] = bool(namespace.get("default", 0)) + return super().__new__(cls, name, bases, namespace) + + +class BinaryParam(int, metaclass=BoolMeta): + @classmethod + def __modify_schema__(cls, field_schema): + field_schema.update( + { + "x-parameter": "bool", + "type": "boolean", + } + ) + + class IntParam(int): def __new__(cls, default: int = 6, minval: float = 1, maxval: float = 10): instance = super().__new__(cls, default) From fe848498bf002c55bbcca035f1d8addf76153e65 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 13 Dec 2023 15:57:39 +0100 Subject: [PATCH 098/267] Update - override BinaryParam subschema --- agenta-cli/agenta/sdk/agenta_decorator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/agenta-cli/agenta/sdk/agenta_decorator.py b/agenta-cli/agenta/sdk/agenta_decorator.py index 6a11594984..12c54ea180 100644 --- a/agenta-cli/agenta/sdk/agenta_decorator.py +++ b/agenta-cli/agenta/sdk/agenta_decorator.py @@ -28,6 +28,7 @@ MessagesInput, FileInputURL, FuncResponse, + BinaryParam, ) app = FastAPI() @@ -362,6 +363,7 @@ def override_schema(openapi_schema: dict, func_name: str, endpoint: str, params: - The default value for DictInput instance - The default value for MessagesParam instance - The default value for FileInputURL instance + - The default value for BinaryParam instance - ... [PLEASE ADD AT EACH CHANGE] Args: @@ -434,3 +436,6 @@ def find_in_schema(schema: dict, param_name: str, xparam: str): ): subschema = find_in_schema(schema_to_override, param_name, "file_url") subschema["default"] = "https://example.com" + if isinstance(param_val, BinaryParam): + subschema = find_in_schema(schema_to_override, param_name, "bool") + subschema["default"] = True if param_val.default == 1 else False From d6a8b82b468b7c6201983b5e3a5353f760b88ea0 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 13 Dec 2023 21:06:38 +0100 Subject: [PATCH 099/267] Update - determine bool type for playground use --- agenta-web/src/lib/helpers/openapi_parser.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agenta-web/src/lib/helpers/openapi_parser.ts b/agenta-web/src/lib/helpers/openapi_parser.ts index 54d7dd34b7..02a3100d68 100644 --- a/agenta-web/src/lib/helpers/openapi_parser.ts +++ b/agenta-web/src/lib/helpers/openapi_parser.ts @@ -63,6 +63,8 @@ const determineType = (xParam: any): string => { return "number" case "dict": return "object" + case "bool": + return "boolean" case "int": return "integer" case "file_url": From 12f255073140e6c152adf9a5a7e43db4c2f532e6 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 13 Dec 2023 22:12:55 +0100 Subject: [PATCH 100/267] Update - include parameter switch ui for boolean param type --- .../Playground/Views/ParametersCards.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/agenta-web/src/components/Playground/Views/ParametersCards.tsx b/agenta-web/src/components/Playground/Views/ParametersCards.tsx index e18a6a570d..53355beccc 100644 --- a/agenta-web/src/components/Playground/Views/ParametersCards.tsx +++ b/agenta-web/src/components/Playground/Views/ParametersCards.tsx @@ -1,8 +1,8 @@ -import {Row, Card, Slider, Select, InputNumber, Col, Input, Button} from "antd" import React from "react" -import {Parameter, InputParameter} from "@/lib/Types" -import {renameVariables} from "@/lib/helpers/utils" import {createUseStyles} from "react-jss" +import {renameVariables} from "@/lib/helpers/utils" +import {Parameter, InputParameter} from "@/lib/Types" +import {Row, Card, Slider, Select, InputNumber, Col, Input, Button, Switch} from "antd" const useStyles = createUseStyles({ row1: { @@ -72,6 +72,10 @@ export const ModelParameters: React.FC = ({ handleParamChange, }) => { const classes = useStyles() + const handleCheckboxChange = (paramName: string, checked: boolean) => { + const value = checked ? 1 : 0 + handleParamChange(paramName, value) + } return ( <> {optParams?.some((param) => !param.input && param.type === "number") && ( @@ -83,7 +87,8 @@ export const ModelParameters: React.FC = ({ !param.input && (param.type === "number" || param.type === "integer" || - param.type === "array"), + param.type === "array") || + param.type === "boolean", ) .map((param, index) => ( @@ -136,6 +141,12 @@ export const ModelParameters: React.FC = ({ ))} )} + {param.type === "boolean" && ( + handleCheckboxChange(param.name, checked)} + /> + )} {param.type === "number" && ( From 14d96724eee9c9f759576b63e650015bc772eb05 Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 14 Dec 2023 09:19:58 +0100 Subject: [PATCH 101/267] :art: Format - ran black and prettier --write . --- agenta-cli/agenta/__init__.py | 2 +- agenta-cli/agenta/sdk/__init__.py | 2 +- .../Playground/Views/ParametersCards.tsx | 14 ++++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/agenta-cli/agenta/__init__.py b/agenta-cli/agenta/__init__.py index 0688097e82..b46ab36d5e 100644 --- a/agenta-cli/agenta/__init__.py +++ b/agenta-cli/agenta/__init__.py @@ -10,7 +10,7 @@ MessagesInput, TextParam, FileInputURL, - BinaryParam + BinaryParam, ) from .sdk.utils.preinit import PreInitObject from .sdk.agenta_init import Config, init diff --git a/agenta-cli/agenta/sdk/__init__.py b/agenta-cli/agenta/sdk/__init__.py index cdea47f0f7..672e09511c 100644 --- a/agenta-cli/agenta/sdk/__init__.py +++ b/agenta-cli/agenta/sdk/__init__.py @@ -12,7 +12,7 @@ TextParam, MessagesInput, FileInputURL, - BinaryParam + BinaryParam, ) from .agenta_init import Config, init from .utils.helper.openai_cost import calculate_token_usage diff --git a/agenta-web/src/components/Playground/Views/ParametersCards.tsx b/agenta-web/src/components/Playground/Views/ParametersCards.tsx index 53355beccc..8a298e0d70 100644 --- a/agenta-web/src/components/Playground/Views/ParametersCards.tsx +++ b/agenta-web/src/components/Playground/Views/ParametersCards.tsx @@ -84,11 +84,11 @@ export const ModelParameters: React.FC = ({ {optParams ?.filter( (param) => - !param.input && - (param.type === "number" || - param.type === "integer" || - param.type === "array") || - param.type === "boolean", + (!param.input && + (param.type === "number" || + param.type === "integer" || + param.type === "array")) || + param.type === "boolean", ) .map((param, index) => ( @@ -144,7 +144,9 @@ export const ModelParameters: React.FC = ({ {param.type === "boolean" && ( handleCheckboxChange(param.name, checked)} + onChange={(checked: boolean) => + handleCheckboxChange(param.name, checked) + } /> )} From 5957b7be309f5fca94c74cd07146d5da7ef445ca Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 14 Dec 2023 18:56:45 +0100 Subject: [PATCH 102/267] Cleanup - remove BinaryParamMixin type --- agenta-cli/agenta/sdk/types.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/agenta-cli/agenta/sdk/types.py b/agenta-cli/agenta/sdk/types.py index 0edab46eb1..28e249fbd6 100644 --- a/agenta-cli/agenta/sdk/types.py +++ b/agenta-cli/agenta/sdk/types.py @@ -42,14 +42,6 @@ def __modify_schema__(cls, field_schema): field_schema.update({"x-parameter": "text"}) -class BinaryParamMixin(BaseModel): - default: bool - - @property - def type(self) -> bool: - return "bool" - - class BoolMeta(type): """ This meta class handles the behavior of a boolean without From 531fd3f0700c2ebeb217f6185b9baa5178c2a0e5 Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 14 Dec 2023 19:13:45 +0100 Subject: [PATCH 103/267] Update - added default value to BoolMeta instance --- agenta-cli/agenta/sdk/types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agenta-cli/agenta/sdk/types.py b/agenta-cli/agenta/sdk/types.py index 28e249fbd6..b64a47f5f8 100644 --- a/agenta-cli/agenta/sdk/types.py +++ b/agenta-cli/agenta/sdk/types.py @@ -53,7 +53,9 @@ def __new__(cls, name: str, bases: tuple, namespace: dict): if "default" in namespace and namespace["default"] not in [0, 1]: raise ValueError("Must provide either 0 or 1") namespace["default"] = bool(namespace.get("default", 0)) - return super().__new__(cls, name, bases, namespace) + instance = super().__new__(cls, name, bases, namespace) + instance.default = 0 + return instance class BinaryParam(int, metaclass=BoolMeta): From ec6029e973c552980578ddb5dbfeb526f17ff629 Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 15 Dec 2023 08:30:34 +0100 Subject: [PATCH 104/267] Update - modified handle checkbox change function --- agenta-web/src/components/Playground/Views/ParametersCards.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/agenta-web/src/components/Playground/Views/ParametersCards.tsx b/agenta-web/src/components/Playground/Views/ParametersCards.tsx index 8a298e0d70..6006ce1349 100644 --- a/agenta-web/src/components/Playground/Views/ParametersCards.tsx +++ b/agenta-web/src/components/Playground/Views/ParametersCards.tsx @@ -73,8 +73,7 @@ export const ModelParameters: React.FC = ({ }) => { const classes = useStyles() const handleCheckboxChange = (paramName: string, checked: boolean) => { - const value = checked ? 1 : 0 - handleParamChange(paramName, value) + handleParamChange(paramName, checked) } return ( <> From 6c3197e028a41eec79578f9ecb60d4d3cea43ff6 Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 15 Dec 2023 08:38:22 +0100 Subject: [PATCH 105/267] Feat - created llm example app for binaryparam --- examples/chat_json_format/app.py | 43 ++++++++++++++++++++++ examples/chat_json_format/requirements.txt | 2 + 2 files changed, 45 insertions(+) create mode 100644 examples/chat_json_format/app.py create mode 100644 examples/chat_json_format/requirements.txt diff --git a/examples/chat_json_format/app.py b/examples/chat_json_format/app.py new file mode 100644 index 0000000000..8f9d234480 --- /dev/null +++ b/examples/chat_json_format/app.py @@ -0,0 +1,43 @@ +import agenta as ag +from agenta.sdk.types import BinaryParam +from openai import OpenAI + +client = OpenAI() + +SYSTEM_PROMPT = "You have expertise in offering technical ideas to startups. Responses should be in json." +GPT_FORMAT_RESPONSE = ["gpt-3.5-turbo-1106", "gpt-4-1106-preview"] +CHAT_LLM_GPT = [ + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-16k-0613", + "gpt-4", +] + GPT_FORMAT_RESPONSE + +ag.init() +ag.config.default( + temperature=ag.FloatParam(0.2), + model=ag.MultipleChoiceParam("gpt-3.5-turbo", CHAT_LLM_GPT), + max_tokens=ag.IntParam(-1, -1, 4000), + prompt_system=ag.TextParam(SYSTEM_PROMPT), + force_json_response=BinaryParam(), +) + + +@ag.entrypoint +def chat(inputs: ag.MessagesInput = ag.MessagesInput()): + messages = [{"role": "system", "content": ag.config.prompt_system}] + inputs + max_tokens = ag.config.max_tokens if ag.config.max_tokens != -1 else None + response_format = ( + {"type": "json_object"} + if ag.config.force_json_response and ag.config.model in GPT_FORMAT_RESPONSE + else {"type": "text"} + ) + chat_completion = client.chat.completions.create( + model=ag.config.model, + messages=messages, + temperature=ag.config.temperature, + max_tokens=max_tokens, + response_format=response_format, + ) + return chat_completion.choices[0].message.content diff --git a/examples/chat_json_format/requirements.txt b/examples/chat_json_format/requirements.txt new file mode 100644 index 0000000000..310f162cec --- /dev/null +++ b/examples/chat_json_format/requirements.txt @@ -0,0 +1,2 @@ +agenta +openai \ No newline at end of file From 9c73a5ad151f171fa881557973ec465151610183 Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 15 Dec 2023 08:41:18 +0100 Subject: [PATCH 106/267] Update - modified handle checkbox change function to fix lint error --- agenta-web/src/components/Playground/Views/ParametersCards.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agenta-web/src/components/Playground/Views/ParametersCards.tsx b/agenta-web/src/components/Playground/Views/ParametersCards.tsx index 6006ce1349..8a298e0d70 100644 --- a/agenta-web/src/components/Playground/Views/ParametersCards.tsx +++ b/agenta-web/src/components/Playground/Views/ParametersCards.tsx @@ -73,7 +73,8 @@ export const ModelParameters: React.FC = ({ }) => { const classes = useStyles() const handleCheckboxChange = (paramName: string, checked: boolean) => { - handleParamChange(paramName, checked) + const value = checked ? 1 : 0 + handleParamChange(paramName, value) } return ( <> From 632518fbf68bc8b4bab0fb0640722685617b0e31 Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 19 Dec 2023 09:52:03 +0100 Subject: [PATCH 107/267] Update - make use of 1/0 in BinaryParam default --- agenta-cli/agenta/sdk/agenta_decorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-cli/agenta/sdk/agenta_decorator.py b/agenta-cli/agenta/sdk/agenta_decorator.py index 12c54ea180..813a3a61d8 100644 --- a/agenta-cli/agenta/sdk/agenta_decorator.py +++ b/agenta-cli/agenta/sdk/agenta_decorator.py @@ -438,4 +438,4 @@ def find_in_schema(schema: dict, param_name: str, xparam: str): subschema["default"] = "https://example.com" if isinstance(param_val, BinaryParam): subschema = find_in_schema(schema_to_override, param_name, "bool") - subschema["default"] = True if param_val.default == 1 else False + subschema["default"] = param_val.default From cf4a6048a1775e074206b1099a2b3c9f3951fbdf Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 19 Dec 2023 18:55:39 +0100 Subject: [PATCH 108/267] Add job extraction templates and tutorials --- docs/cookbook/extract_job_information.mdx | 49 +++++++++++++++++++ .../list_templates_by_architecture.mdx | 15 ++++++ .../cookbook/list_templates_by_technology.mdx | 15 ++++++ docs/cookbook/list_templates_by_use_case.mdx | 7 +++ 4 files changed, 86 insertions(+) create mode 100644 docs/cookbook/extract_job_information.mdx create mode 100644 docs/cookbook/list_templates_by_architecture.mdx create mode 100644 docs/cookbook/list_templates_by_technology.mdx create mode 100644 docs/cookbook/list_templates_by_use_case.mdx diff --git a/docs/cookbook/extract_job_information.mdx b/docs/cookbook/extract_job_information.mdx new file mode 100644 index 0000000000..19f6d634fb --- /dev/null +++ b/docs/cookbook/extract_job_information.mdx @@ -0,0 +1,49 @@ +--- +title: "Extraction using OpenAI Functions and Langchain" +--- + +This templates is designed to extracts job information (company name, job title, salary range) from a job description. It uses OpenAI Functions and Langchain. + +## Code base + +The code base can be found in the [GitHub repository](https://github.com/Agenta-AI/job_extractor_template). + +## How to use +### 0. Prerequisites +- Install the agenta CLI +```bash +pip install agenta-cli +``` +- Either create an account in [agenta cloud](https://cloud.agenta.ai/) or [self-host agenta](/self-host/host-locally) + +### 1. Clone the repository + +```bash +git clone https://github.com/Agenta-AI/job_extractor_template +``` + +### 2. Initialize the project + +```bash +agenta init +``` + +### 3. Setup your openAI API key +Create a .env file by copying the .env.example file and add your openAI API key to it. +```bash +OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 4. Deploy the application to agenta + +```bash +agenta variant serve app.py +``` + +### 5. Experiment with the prompts in a playground and evaluate different variants + diff --git a/docs/cookbook/list_templates_by_architecture.mdx b/docs/cookbook/list_templates_by_architecture.mdx new file mode 100644 index 0000000000..498839fdfd --- /dev/null +++ b/docs/cookbook/list_templates_by_architecture.mdx @@ -0,0 +1,15 @@ +--- +title: "Templates by Architecture" +description: "A collection of templates and tutorials indexed by architecture." +--- + This page is a work in progress. Please note that some of the entries are redundant. + +# Tutorials + +# Templates +## ⛏️ Extraction + +These templates extract data in a structured format from an unstructured source. + +### [Extraction using OpenAI Functions and Langchain](/cookbook/extract_job_information) +Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. diff --git a/docs/cookbook/list_templates_by_technology.mdx b/docs/cookbook/list_templates_by_technology.mdx new file mode 100644 index 0000000000..7548e4f873 --- /dev/null +++ b/docs/cookbook/list_templates_by_technology.mdx @@ -0,0 +1,15 @@ +--- +title: "Templates by Technology" +description: "A collection of templates and tutorials indexed by the used framework and model provider." +--- + + This page is a work in progress. Please note that some of the entries are redundant. + +## Langchain +### [Extraction using OpenAI Functions and Langchain](/templates/extract_job_information) +Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. + + +## OpenAI +### [Extraction using OpenAI Functions and Langchain](/templates/extract_job_information) +Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. diff --git a/docs/cookbook/list_templates_by_use_case.mdx b/docs/cookbook/list_templates_by_use_case.mdx new file mode 100644 index 0000000000..c9d7e955d9 --- /dev/null +++ b/docs/cookbook/list_templates_by_use_case.mdx @@ -0,0 +1,7 @@ +--- +title: "Templates by Use Case" +description: "A collection of templates and tutorials indexed by the the use case." +--- + This page is a work in progress. Please note that some of the entries are redundant. + +## Human Ressources \ No newline at end of file From cdeebf08ecee4765925d48022022c98bcf1e41fe Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 19 Dec 2023 18:55:43 +0100 Subject: [PATCH 109/267] Add BinaryParam to config_datatypes.mdx --- docs/sdk/config_datatypes.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/sdk/config_datatypes.mdx b/docs/sdk/config_datatypes.mdx index 3f0de2e078..98a261b93a 100644 --- a/docs/sdk/config_datatypes.mdx +++ b/docs/sdk/config_datatypes.mdx @@ -35,7 +35,17 @@ agenta.config.default(temperature = ag.IntParam(default=0.5, minval=0, maxval=2) temperature2 = ag.IntParam(0.5) ``` +### BinaryParam +This displays a binary switch in the playground. + + +```python +agenta.config.default(temperature = ag.IntParam(default=0.5, minval=0, maxval=2), + force_json = BinaryParam()) +``` + + For now the binary parameter is always initialized with `False` and can only be changed from the playground ## Data types for inputs Inputs in contrast to parameters are given as argument to the function decorated with `@agenta.entrypoint`. They are not part of the configuration but instead are the input in the call to the LLM app. From 17afbf60adb5239c0dddd534480318bf8f70dd84 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 19 Dec 2023 18:55:48 +0100 Subject: [PATCH 110/267] Add Cookbook section to mint.json --- docs/mint.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/mint.json b/docs/mint.json index 10bff3da78..a0457adb22 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -231,6 +231,20 @@ "pages": [ "changelog/main" ] + }, + { + "group": "Cookbook", + "pages": [ + "cookbook/list_templates_by_architecture", + "cookbook/list_templates_by_technology", + "cookbook/list_templates_by_use_case", + { + "group": "Templates", + "pages": [ + "cookbook/extract_job_information" + ] + } + ] } ], "api": { From 111f1ca6a688f23b5c6fc9d8fe64ee69fa4c591c Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 19 Dec 2023 18:58:17 +0100 Subject: [PATCH 111/267] Add async version of baby name generator app --- examples/baby_name_generator/app.py | 2 +- examples/baby_name_generator/app_async.py | 34 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 examples/baby_name_generator/app_async.py diff --git a/examples/baby_name_generator/app.py b/examples/baby_name_generator/app.py index b51c96ee95..49e4aced68 100644 --- a/examples/baby_name_generator/app.py +++ b/examples/baby_name_generator/app.py @@ -1,8 +1,8 @@ +from agenta import FloatParam, TextParam import agenta as ag from openai import OpenAI client = OpenAI() -from agenta import FloatParam, TextParam default_prompt = ( "Give me 10 names for a baby from this country {country} with gender {gender}!!!!" diff --git a/examples/baby_name_generator/app_async.py b/examples/baby_name_generator/app_async.py new file mode 100644 index 0000000000..d28335aa9e --- /dev/null +++ b/examples/baby_name_generator/app_async.py @@ -0,0 +1,34 @@ +from agenta import FloatParam, TextParam +import agenta as ag +from openai import AsyncOpenAI + +client = AsyncOpenAI() + +default_prompt = ( + "Give me 10 names for a baby from this country {country} with gender {gender}!!!!" +) + +ag.init() +ag.config.default( + temperature=FloatParam(0.2), prompt_template=TextParam(default_prompt) +) + + +@ag.entrypoint +async def generate(country: str, gender: str) -> str: + """ + Generate a baby name based on the given country and gender. + + Args: + country (str): The country to generate the name from. + gender (str): The gender of the baby. + + Returns: + str: The generated baby name. + """ + prompt = ag.config.prompt_template.format(country=country, gender=gender) + + chat_completion = await client.chat.completions.create( + model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}] + ) + return chat_completion.choices[0].message.content From 50f67bac0dd735afabe659e9cb0348b31a476fda Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 19 Dec 2023 18:58:26 +0100 Subject: [PATCH 112/267] Update job_info_extractor README and app.py --- examples/job_info_extractor/README.md | 52 +++++++++++++++++++++++---- examples/job_info_extractor/app.py | 17 ++++++--- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/examples/job_info_extractor/README.md b/examples/job_info_extractor/README.md index 757455e2ca..4b0b51cc61 100644 --- a/examples/job_info_extractor/README.md +++ b/examples/job_info_extractor/README.md @@ -1,9 +1,49 @@ -# Using this template +# Extraction using OpenAI Functions and Langchain" -Please make sure to create a `.env` file with your OpenAI API key before running the app. -OPENAI_API_KEY=sk-xxxxxxx -You can find your keys here: -https://platform.openai.com/account/api-keys +This templates is designed to extracts job information (company name, job +title, salary range) from a job description. It uses OpenAI Functions and +Langchain. It runs with agenta. +[Agenta](https://github.com/agenta-ai/agenta) is an open-source LLMOps +platform that allows you to 1) quickly experiment and compare +configuration for LLM apps 2) evaluate prompts and workflows 3) deploy +applications easily. + +## How to use +### 0. Prerequisites +- Install the agenta CLI +```bash +pip install agenta-cli +``` +- Either create an account in [agenta cloud](https://cloud.agenta.ai/) or +[self-host agenta](/self-host/host-locally) + +### 1. Clone the repository + +```bash +git clone https://github.com/Agenta-AI/job_extractor_template +``` + +### 2. Initialize the project + +```bash +agenta init +``` + +### 3. Setup your openAI API key +Create a .env file by copying the .env.example file and add your openAI +API key to it. +```bash +OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 4. Deploy the application to agenta + +```bash +agenta variant serve app.py +``` + +### 5. Experiment with the prompts in a playground and evaluate different variants in agenta + +https://github.com/Agenta-AI/job_extractor_template/assets/4510758/30271188-8d46-4d02-8207-ddb60ad0e284 -Go back to the [Getting started tutorial](https://docs.agenta.ai/getting-started) to continue \ No newline at end of file diff --git a/examples/job_info_extractor/app.py b/examples/job_info_extractor/app.py index 4ccd38a583..8d4dd21941 100644 --- a/examples/job_info_extractor/app.py +++ b/examples/job_info_extractor/app.py @@ -11,11 +11,17 @@ from pydantic import BaseModel, Field -default_prompt = "What is a good name for a company that makes {product}?" +CHAT_LLM_GPT = [ + "gpt-3.5-turbo-16k-0613", + "gpt-3.5-turbo-16k", + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-0301", + "gpt-3.5-turbo", + "gpt-4", +] ag.init() ag.config.default( - prompt_template=ag.TextParam(default_prompt), system_message=ag.TextParam( "You are a world class algorithm for extracting information in structured formats." ), @@ -26,10 +32,11 @@ company_desc_message=ag.TextParam("The name of the company"), position_desc_message=ag.TextParam("The name of the position"), salary_range_desc_message=ag.TextParam("The salary range of the position"), - temperature=ag.FloatParam(0.5), - top_p=ag.FloatParam(1.0), + temperature=ag.FloatParam(0.9), + top_p=ag.FloatParam(0.9), presence_penalty=ag.FloatParam(0.0), frequency_penalty=ag.FloatParam(0.0), + model=ag.MultipleChoiceParam("gpt-3.5-turbo-0613", CHAT_LLM_GPT), ) @@ -50,7 +57,7 @@ def generate( ) -> str: """Extract information from a job description""" llm = ChatOpenAI( - model="gpt-3.5-turbo-0613", + model=ag.config.model, temperature=ag.config.temperature, top_p=ag.config.top_p, presence_penalty=ag.config.presence_penalty, From 3c64c78dc44ed6789cc49a4d7ebbcc56505255d6 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Wed, 20 Dec 2023 13:35:02 +0100 Subject: [PATCH 113/267] Update pyproject.toml version to 0.6.7 and update fastapi and ipdb dependencies --- agenta-cli/poetry.lock | 632 ++++++++++++++++++++++---------------- agenta-cli/pyproject.toml | 6 +- 2 files changed, 364 insertions(+), 274 deletions(-) diff --git a/agenta-cli/poetry.lock b/agenta-cli/poetry.lock index 940e1ffff7..deafab2322 100644 --- a/agenta-cli/poetry.lock +++ b/agenta-cli/poetry.lock @@ -1,52 +1,54 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + [[package]] name = "anyio" -version = "3.6.2" +version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" files = [ - {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, - {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, ] [package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] - -[[package]] -name = "appnope" -version = "0.1.3" -description = "Disable App Nap on macOS >= 10.9" -optional = false -python-versions = "*" -files = [ - {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, - {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, -] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] [[package]] name = "asttokens" -version = "2.2.1" +version = "2.4.1" description = "Annotate AST trees with source code positions" optional = false python-versions = "*" files = [ - {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, - {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, ] [package.dependencies] -six = "*" +six = ">=1.12.0" [package.extras] -test = ["astroid", "pytest"] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] [[package]] name = "atomicwrites" @@ -76,17 +78,6 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope-interface"] tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -[[package]] -name = "backcall" -version = "0.2.0" -description = "Specifications for callback functions passed in to an API" -optional = false -python-versions = "*" -files = [ - {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, - {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, -] - [[package]] name = "backoff" version = "2.2.1" @@ -100,108 +91,123 @@ files = [ [[package]] name = "certifi" -version = "2023.5.7" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, - {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] [[package]] name = "charset-normalizer" -version = "3.1.0" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "click" -version = "8.1.3" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -231,13 +237,13 @@ files = [ [[package]] name = "docker" -version = "6.1.1" +version = "6.1.3" description = "A Python library for the Docker Engine API." optional = false python-versions = ">=3.7" files = [ - {file = "docker-6.1.1-py3-none-any.whl", hash = "sha256:8308b23d3d0982c74f7aa0a3abd774898c0c4fba006e9c3bde4f68354e470fe2"}, - {file = "docker-6.1.1.tar.gz", hash = "sha256:5ec18b9c49d48ee145a5b5824bb126dc32fc77931e18444783fc07a7724badc0"}, + {file = "docker-6.1.3-py3-none-any.whl", hash = "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9"}, + {file = "docker-6.1.3.tar.gz", hash = "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20"}, ] [package.dependencies] @@ -251,69 +257,82 @@ websocket-client = ">=0.32.0" ssh = ["paramiko (>=2.4.3)"] [[package]] -name = "executing" +name = "exceptiongroup" version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "2.0.1" description = "Get the currently executing AST node of a frame, and other information" optional = false -python-versions = "*" +python-versions = ">=3.5" files = [ - {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, - {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, ] [package.extras] -tests = ["asttokens", "littleutils", "pytest", "rich"] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] [[package]] name = "fastapi" -version = "0.95.1" +version = "0.105.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "fastapi-0.95.1-py3-none-any.whl", hash = "sha256:a870d443e5405982e1667dfe372663abf10754f246866056336d7f01c21dab07"}, - {file = "fastapi-0.95.1.tar.gz", hash = "sha256:9569f0a381f8a457ec479d90fa01005cfddaae07546eb1f3fa035bc4797ae7d5"}, + {file = "fastapi-0.105.0-py3-none-any.whl", hash = "sha256:f19ebf6fdc82a3281d10f2cb4774bdfa90238e3b40af3525a0c09fd08ad1c480"}, + {file = "fastapi-0.105.0.tar.gz", hash = "sha256:4d12838819aa52af244580675825e750ad67c9df4614f557a769606af902cf22"}, ] [package.dependencies] -pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = ">=0.26.1,<0.27.0" +anyio = ">=3.7.1,<4.0.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.27.0,<0.28.0" +typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "importlib-metadata" -version = "6.7.0" +version = "6.11.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, - {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, + {file = "importlib_metadata-6.11.0-py3-none-any.whl", hash = "sha256:f0afba6205ad8f8947c7d338b5342d5db2afbfd82f9cbef7879a9539cc12eb9b"}, + {file = "importlib_metadata-6.11.0.tar.gz", hash = "sha256:1231cf92d825c9e03cfc4da076a16de6422c863558229ea0b22b675657463443"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] [[package]] name = "iniconfig" @@ -344,61 +363,59 @@ tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < [[package]] name = "ipython" -version = "8.13.2" +version = "8.18.1" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.9" files = [ - {file = "ipython-8.13.2-py3-none-any.whl", hash = "sha256:ffca270240fbd21b06b2974e14a86494d6d29290184e788275f55e0b55914926"}, - {file = "ipython-8.13.2.tar.gz", hash = "sha256:7dff3fad32b97f6488e02f87b970f309d082f758d7b7fc252e3b19ee0e432dbb"}, + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, ] [package.dependencies] -appnope = {version = "*", markers = "sys_platform == \"darwin\""} -backcall = "*" colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} jedi = ">=0.16" matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -pickleshare = "*" -prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5" typing-extensions = {version = "*", markers = "python_version < \"3.10\""} [package.extras] -all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] black = ["black"] -doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] [[package]] name = "jedi" -version = "0.18.2" +version = "0.19.1" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.6" files = [ - {file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"}, - {file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"}, + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, ] [package.dependencies] -parso = ">=0.8.0,<0.9.0" +parso = ">=0.8.3,<0.9.0" [package.extras] docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "matplotlib-inline" @@ -427,13 +444,13 @@ files = [ [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] @@ -453,38 +470,27 @@ testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "pexpect" -version = "4.8.0" +version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" files = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, ] [package.dependencies] ptyprocess = ">=0.5" -[[package]] -name = "pickleshare" -version = "0.7.5" -description = "Tiny 'shelve'-like database with concurrency support" -optional = false -python-versions = "*" -files = [ - {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, - {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, -] - [[package]] name = "pluggy" -version = "1.0.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] @@ -493,13 +499,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "posthog" -version = "3.0.2" +version = "3.1.0" description = "Integrate PostHog into any python application." optional = false python-versions = "*" files = [ - {file = "posthog-3.0.2-py2.py3-none-any.whl", hash = "sha256:a8c0af6f2401fbe50f90e68c4143d0824b54e872de036b1c2f23b5abb39d88ce"}, - {file = "posthog-3.0.2.tar.gz", hash = "sha256:701fba6e446a4de687c6e861b587e7b7741955ad624bf34fe013c06a0fec6fb3"}, + {file = "posthog-3.1.0-py2.py3-none-any.whl", hash = "sha256:acd033530bdfc275dce5587f205f62378991ecb9b7cd5479e79c7f4ac575d319"}, + {file = "posthog-3.1.0.tar.gz", hash = "sha256:db17a2c511e18757aec12b6632ddcc1fa318743dad88a4666010467a3d9468da"}, ] [package.dependencies] @@ -512,17 +518,17 @@ six = ">=1.5" [package.extras] dev = ["black", "flake8", "flake8-print", "isort", "pre-commit"] sentry = ["django", "sentry-sdk"] -test = ["coverage", "flake8", "freezegun (==0.3.15)", "mock (>=2.0.0)", "pylint", "pytest"] +test = ["coverage", "flake8", "freezegun (==0.3.15)", "mock (>=2.0.0)", "pylint", "pytest", "pytest-timeout"] [[package]] name = "prompt-toolkit" -version = "3.0.38" +version = "3.0.43" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"}, - {file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"}, + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, ] [package.dependencies] @@ -566,69 +572,154 @@ files = [ [[package]] name = "pydantic" -version = "1.10.7" -description = "Data validation and settings management using python type hints" +version = "2.5.2" +description = "Data validation using Python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, - {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, - {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, - {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, - {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, - {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, - {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, - {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, - {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, - {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, - {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, - {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, - {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, - {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, - {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, - {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, - {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, - {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, - {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, - {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, - {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, - {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, - {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, - {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, - {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, - {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, - {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, - {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, - {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, - {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, - {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, - {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, - {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, - {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, - {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, - {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, + {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, + {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.14.5" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.14.5" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, + {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, + {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, + {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, + {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, + {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, + {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, + {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, + {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, + {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, + {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, + {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, + {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, + {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, + {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, + {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, + {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, + {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, + {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, + {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325"}, + {file = "pydantic_core-2.14.5-cp37-none-win32.whl", hash = "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405"}, + {file = "pydantic_core-2.14.5-cp37-none-win_amd64.whl", hash = "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588"}, + {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, + {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, + {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, + {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, + {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, + {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, + {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, + {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, + {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, + {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, + {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, + {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, + {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" -version = "2.15.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, - {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" @@ -738,13 +829,13 @@ docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphin [[package]] name = "requests" -version = "2.30.0" +version = "2.31.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" files = [ - {file = "requests-2.30.0-py3-none-any.whl", hash = "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294"}, - {file = "requests-2.30.0.tar.gz", hash = "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] @@ -781,13 +872,13 @@ files = [ [[package]] name = "stack-data" -version = "0.6.2" +version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" optional = false python-versions = "*" files = [ - {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, - {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, ] [package.dependencies] @@ -800,13 +891,13 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "starlette" -version = "0.26.1" +version = "0.27.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.7" files = [ - {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"}, - {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"}, + {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, + {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, ] [package.dependencies] @@ -840,90 +931,89 @@ files = [ [[package]] name = "traitlets" -version = "5.9.0" +version = "5.14.0" description = "Traitlets Python configuration system" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, - {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, + {file = "traitlets-5.14.0-py3-none-any.whl", hash = "sha256:f14949d23829023013c47df20b4a76ccd1a85effb786dc060f34de7948361b33"}, + {file = "traitlets-5.14.0.tar.gz", hash = "sha256:fcdaa8ac49c04dfa0ed3ee3384ef6dfdb5d6f3741502be247279407679296772"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] name = "urllib3" -version = "2.0.2" +version = "2.1.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.2-py3-none-any.whl", hash = "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e"}, - {file = "urllib3-2.0.2.tar.gz", hash = "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc"}, + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "wcwidth" -version = "0.2.6" +version = "0.2.12" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, - {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, + {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, + {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, ] [[package]] name = "websocket-client" -version = "1.5.1" +version = "1.7.0" description = "WebSocket client for Python with low level API options" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "websocket-client-1.5.1.tar.gz", hash = "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40"}, - {file = "websocket_client-1.5.1-py3-none-any.whl", hash = "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e"}, + {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, + {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, ] [package.extras] -docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] [[package]] name = "zipp" -version = "3.15.0" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "1a41135d3e71717b16a1d1d3d1b8523274ae2f816ca9ffceea585a69bc6420dd" +content-hash = "e380d5f47d97cb6e21711805c84421fd0634397664cc2c904c140416d0f2635c" diff --git a/agenta-cli/pyproject.toml b/agenta-cli/pyproject.toml index 107fe27e4d..0ef5fc6f19 100644 --- a/agenta-cli/pyproject.toml +++ b/agenta-cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "agenta" -version = "0.6.6" +version = "0.6.7" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" authors = ["Mahmoud Mabrouk "] @@ -19,10 +19,10 @@ keywords = ["LLMOps", "LLM", "evaluation", "prompt engineering"] python = "^3.9" docker = "^6.1.1" click = "^8.1.3" -fastapi = "^0.95.1" +fastapi = ">=0.95" toml = "^0.10.2" questionary = "^1.10.0" -ipdb = "^0.13.13" +ipdb = ">=0.13" python-dotenv = "^1.0.0" python-multipart = "^0.0.6" importlib-metadata = "^6.7.0" From 4ee934c25bca140c30a3ee49c0ec5917441f8edd Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Wed, 20 Dec 2023 19:33:44 +0100 Subject: [PATCH 114/267] Update README.md --- README.md | 58 +++++++++++-------------------------------------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 7504a60207..d11466c44e 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,11 @@

About • - DemoQuick StartInstallationFeaturesDocumentation • - Support • + EnterpriseCommunityContributing

@@ -94,54 +93,19 @@ # ℹ️ About -Building production-ready LLM-powered applications is currently very difficult. It involves countless iterations of prompt engineering, parameter tuning, and architectures. +Agenta is an end-to-end LLMOps platform. It provides the tools for **prompt engineering and management**, ⚖️ **evaluation**, and :rocket: **deployment**. All without imposing any restrictions on your choice of framework, library, or model. -Agenta provides you with the tools to quickly do prompt engineering and 🧪 **experiment**, ⚖️ **evaluate**, and :rocket: **deploy** your LLM apps. All without imposing any restrictions on your choice of framework, library, or model. +Agenta allows developers and product teams to collaborate and build robust AI applications in less time.

-
- - - - Overview agenta - -
- - -# Demo -https://github.com/Agenta-AI/agenta/assets/57623556/99733147-2b78-4b95-852f-67475e4ce9ed # Quick Start - - +### [Try the cloud version](https://cloud.agenta.ai?utm_source=github&utm_medium=readme&utm_campaign=github) +### [Create your first application in one-minute](https://docs.agenta.ai/quickstart/getting-started-ui) +### [Create an application using Langchain](https://docs.agenta.ai/tutorials/first-app-with-langchain) +### [Self-host agenta](https://docs.agenta.ai/self-host/host-locally) +### [Read the Documentation](https://docs.agenta.ai) +### [Check the Cookbook](https://docs.agenta.ai/cookbook) # Features @@ -216,8 +180,8 @@ Now your team can 🔄 iterate, 🧪 experiment, and ⚖️ evaluate different v Screenshot 2023-06-25 at 21 08 53 -# Support -Talk with the founders for any commercial inquiries.

+# Enterprise Support +Contact us here for enterprise support and early access to agenta self-managed enterprise with Kubernetes support.

Book us # Disabling Anonymized Tracking From 7f7e1d71e03dd84cac0200b0d421e8b82c6bae14 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 20 Dec 2023 00:32:44 +0100 Subject: [PATCH 115/267] Update - modified BinaryParam sdk type --- agenta-cli/agenta/sdk/types.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/agenta-cli/agenta/sdk/types.py b/agenta-cli/agenta/sdk/types.py index b64a47f5f8..a3f6b39eba 100644 --- a/agenta-cli/agenta/sdk/types.py +++ b/agenta-cli/agenta/sdk/types.py @@ -42,23 +42,12 @@ def __modify_schema__(cls, field_schema): field_schema.update({"x-parameter": "text"}) -class BoolMeta(type): - """ - This meta class handles the behavior of a boolean without - directly inheriting from it (avoiding the conflict - that comes from inheriting bool). - """ - - def __new__(cls, name: str, bases: tuple, namespace: dict): - if "default" in namespace and namespace["default"] not in [0, 1]: - raise ValueError("Must provide either 0 or 1") - namespace["default"] = bool(namespace.get("default", 0)) - instance = super().__new__(cls, name, bases, namespace) - instance.default = 0 +class BinaryParam(int): + def __new__(cls, value: bool = False): + instance = super().__new__(cls, int(value)) + instance.default = value return instance - -class BinaryParam(int, metaclass=BoolMeta): @classmethod def __modify_schema__(cls, field_schema): field_schema.update( From e365aa57101446581c8ef9da5366fb145329742d Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 20 Dec 2023 00:33:13 +0100 Subject: [PATCH 116/267] Update - include boolean type in handleParamChange function --- .../src/components/Playground/Views/ParametersCards.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/agenta-web/src/components/Playground/Views/ParametersCards.tsx b/agenta-web/src/components/Playground/Views/ParametersCards.tsx index 8a298e0d70..aaffb16bb5 100644 --- a/agenta-web/src/components/Playground/Views/ParametersCards.tsx +++ b/agenta-web/src/components/Playground/Views/ParametersCards.tsx @@ -63,7 +63,7 @@ const useStyles = createUseStyles({ interface ModelParametersProps { optParams: Parameter[] | null onChange: (param: Parameter, value: number | string) => void - handleParamChange: (name: string, value: number | string) => void + handleParamChange: (name: string, value: number | string | boolean) => void } export const ModelParameters: React.FC = ({ @@ -73,8 +73,7 @@ export const ModelParameters: React.FC = ({ }) => { const classes = useStyles() const handleCheckboxChange = (paramName: string, checked: boolean) => { - const value = checked ? 1 : 0 - handleParamChange(paramName, value) + handleParamChange(paramName, checked) } return ( <> From 204bf0701b3a2d55d3d9c37086109792ef67a85c Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 20 Dec 2023 00:34:05 +0100 Subject: [PATCH 117/267] Update - modified default parameter --- examples/chat_json_format/app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/chat_json_format/app.py b/examples/chat_json_format/app.py index 8f9d234480..a47f659c35 100644 --- a/examples/chat_json_format/app.py +++ b/examples/chat_json_format/app.py @@ -1,5 +1,4 @@ import agenta as ag -from agenta.sdk.types import BinaryParam from openai import OpenAI client = OpenAI() @@ -20,7 +19,7 @@ model=ag.MultipleChoiceParam("gpt-3.5-turbo", CHAT_LLM_GPT), max_tokens=ag.IntParam(-1, -1, 4000), prompt_system=ag.TextParam(SYSTEM_PROMPT), - force_json_response=BinaryParam(), + force_json_response=ag.BinaryParam(False), ) From 73e829b744ca6b31901ffb048d1bef6c1226a910 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Wed, 20 Dec 2023 20:09:37 +0100 Subject: [PATCH 118/267] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d11466c44e..8a47183c37 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,12 @@ Agenta is an end-to-end LLMOps platform. It provides the tools for **prompt engineering and management**, ⚖️ **evaluation**, and :rocket: **deployment**. All without imposing any restrictions on your choice of framework, library, or model. Agenta allows developers and product teams to collaborate and build robust AI applications in less time. + +## How does it work? + +| Using an LLM App Template (For Non-Technical Users) | Starting from Code | +| ------------- | ------------- | +|1. Create an application using a pre-built template from our UI.
2. Access a playground where you can test and compare different prompts and configurations side-by-side.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |1. Add a few lines to any LLM application code to automatically create a playground for it.
2. Experiment with prompts and configurations, and compare them side-by-side in the playground.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |

# Quick Start From eb8a9b70dae0f6927f751aa8123331fa3053180a Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Wed, 20 Dec 2023 20:11:44 +0100 Subject: [PATCH 119/267] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8a47183c37..def4a4dc94 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,12 @@ Agenta is an end-to-end LLMOps platform. It provides the tools for **prompt engi Agenta allows developers and product teams to collaborate and build robust AI applications in less time. -## How does it work? +## 🔨 How does it work? | Using an LLM App Template (For Non-Technical Users) | Starting from Code | | ------------- | ------------- | -|1. Create an application using a pre-built template from our UI.
2. Access a playground where you can test and compare different prompts and configurations side-by-side.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |1. Add a few lines to any LLM application code to automatically create a playground for it.
2. Experiment with prompts and configurations, and compare them side-by-side in the playground.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. | +|1. [Create an application using a pre-built template from our UI](https://cloud.agenta.ai?utm_source=github&utm_medium=readme&utm_campaign=github)br />2. Access a playground where you can test and compare different prompts and configurations side-by-side.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |1. [Add a few lines to any LLM application code to automatically create a playground for it](https://docs.agenta.ai/tutorials/first-app-with-langchain)
2. Experiment with prompts and configurations, and compare them side-by-side in the playground.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. | +

# Quick Start From c4e0be704b28137b802f2c1a0cd440f394c92953 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Wed, 20 Dec 2023 20:12:03 +0100 Subject: [PATCH 120/267] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index def4a4dc94..ba3770ee1a 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Agenta allows developers and product teams to collaborate and build robust AI ap | Using an LLM App Template (For Non-Technical Users) | Starting from Code | | ------------- | ------------- | -|1. [Create an application using a pre-built template from our UI](https://cloud.agenta.ai?utm_source=github&utm_medium=readme&utm_campaign=github)br />2. Access a playground where you can test and compare different prompts and configurations side-by-side.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |1. [Add a few lines to any LLM application code to automatically create a playground for it](https://docs.agenta.ai/tutorials/first-app-with-langchain)
2. Experiment with prompts and configurations, and compare them side-by-side in the playground.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. | +|1. [Create an application using a pre-built template from our UI](https://cloud.agenta.ai?utm_source=github&utm_medium=readme&utm_campaign=github)
2. Access a playground where you can test and compare different prompts and configurations side-by-side.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |1. [Add a few lines to any LLM application code to automatically create a playground for it](https://docs.agenta.ai/tutorials/first-app-with-langchain)
2. Experiment with prompts and configurations, and compare them side-by-side in the playground.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |

From ccd5c0ae74709ee765e5488212bfadf79a91ae8c Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 21 Dec 2023 18:13:49 +0100 Subject: [PATCH 121/267] Bump fastapi version to 0.95.1 --- agenta-cli/poetry.lock | 213 +++++++++++--------------------------- agenta-cli/pyproject.toml | 4 +- 2 files changed, 62 insertions(+), 155 deletions(-) diff --git a/agenta-cli/poetry.lock b/agenta-cli/poetry.lock index deafab2322..6851b2490e 100644 --- a/agenta-cli/poetry.lock +++ b/agenta-cli/poetry.lock @@ -1,36 +1,26 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. -[[package]] -name = "annotated-types" -version = "0.6.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, -] - [[package]] name = "anyio" -version = "3.7.1" +version = "4.2.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, ] [package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "asttokens" @@ -286,23 +276,24 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "fastapi" -version = "0.105.0" +version = "0.95.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "fastapi-0.105.0-py3-none-any.whl", hash = "sha256:f19ebf6fdc82a3281d10f2cb4774bdfa90238e3b40af3525a0c09fd08ad1c480"}, - {file = "fastapi-0.105.0.tar.gz", hash = "sha256:4d12838819aa52af244580675825e750ad67c9df4614f557a769606af902cf22"}, + {file = "fastapi-0.95.2-py3-none-any.whl", hash = "sha256:d374dbc4ef2ad9b803899bd3360d34c534adc574546e25314ab72c0c4411749f"}, + {file = "fastapi-0.95.2.tar.gz", hash = "sha256:4d9d3e8c71c73f11874bcf5e33626258d143252e329a01002f767306c64fb982"}, ] [package.dependencies] -anyio = ">=3.7.1,<4.0.0" -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" starlette = ">=0.27.0,<0.28.0" -typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] [[package]] name = "idna" @@ -572,139 +563,55 @@ files = [ [[package]] name = "pydantic" -version = "2.5.2" -description = "Data validation using Python type hints" +version = "1.10.13" +description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, - {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, + {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, + {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, + {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, + {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, + {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, + {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, + {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, + {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, + {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, + {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, + {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, + {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, + {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, + {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, + {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, + {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, + {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, + {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, + {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, + {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, + {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, + {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.14.5" -typing-extensions = ">=4.6.1" +typing-extensions = ">=4.2.0" [package.extras] -email = ["email-validator (>=2.0.0)"] - -[[package]] -name = "pydantic-core" -version = "2.14.5" -description = "" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, - {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, - {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, - {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, - {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, - {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, - {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, - {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, - {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4"}, - {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325"}, - {file = "pydantic_core-2.14.5-cp37-none-win32.whl", hash = "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405"}, - {file = "pydantic_core-2.14.5-cp37-none-win_amd64.whl", hash = "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, - {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, - {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, - {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, - {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, - {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, - {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, - {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, - {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, - {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, - {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, - {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] [[package]] name = "pygments" @@ -1016,4 +923,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "e380d5f47d97cb6e21711805c84421fd0634397664cc2c904c140416d0f2635c" +content-hash = "0763bb5af9bbf57a84f08a6c1e52f6396a8423bb703848a767cb13473aedc13a" diff --git a/agenta-cli/pyproject.toml b/agenta-cli/pyproject.toml index 0ef5fc6f19..288255a3a6 100644 --- a/agenta-cli/pyproject.toml +++ b/agenta-cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "agenta" -version = "0.6.7" +version = "0.6.8" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" authors = ["Mahmoud Mabrouk "] @@ -19,7 +19,7 @@ keywords = ["LLMOps", "LLM", "evaluation", "prompt engineering"] python = "^3.9" docker = "^6.1.1" click = "^8.1.3" -fastapi = ">=0.95" +fastapi = "^0.95.1" toml = "^0.10.2" questionary = "^1.10.0" ipdb = ">=0.13" From b04e5dc7e60afa387fdf3780c593c0cc1a8fd112 Mon Sep 17 00:00:00 2001 From: Romain Brucker Date: Fri, 22 Dec 2023 14:52:02 +0100 Subject: [PATCH 122/267] fix: setting docker to always restart for both base-containers and apps --- agenta-backend/agenta_backend/services/docker_utils.py | 1 + docker-compose.gh.yml | 7 +++++++ docker-compose.prod.yml | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/agenta-backend/agenta_backend/services/docker_utils.py b/agenta-backend/agenta_backend/services/docker_utils.py index 73c0b88a3e..df5623f14b 100644 --- a/agenta-backend/agenta_backend/services/docker_utils.py +++ b/agenta-backend/agenta_backend/services/docker_utils.py @@ -114,6 +114,7 @@ def start_container( name=container_name, environment=env_vars, extra_hosts=extra_hosts, + restart_policy={"Name": "always"}, ) # Check the container's status sleep(0.5) diff --git a/docker-compose.gh.yml b/docker-compose.gh.yml index 35e3642b42..563bd8e099 100644 --- a/docker-compose.gh.yml +++ b/docker-compose.gh.yml @@ -9,6 +9,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock networks: - agenta-network + restart: always agenta-backend: container_name: agenta-backend-1 @@ -52,6 +53,7 @@ services: depends_on: mongo: condition: service_healthy + restart: always agenta-web: container_name: agenta-web-1 @@ -69,6 +71,8 @@ services: - NEXT_PUBLIC_AGENTA_API_URL=${DOMAIN_NAME:-http://localhost} - NEXT_PUBLIC_FF=oss - NEXT_PUBLIC_TELEMETRY_TRACKING_ENABLED=true + restart: always + mongo: image: mongo:5.0 environment: @@ -85,6 +89,7 @@ services: interval: 10s timeout: 10s retries: 20 + restart: always mongo_express: image: mongo-express @@ -99,6 +104,7 @@ services: depends_on: mongo: condition: service_healthy + restart: always redis: image: redis:latest @@ -106,6 +112,7 @@ services: - agenta-network volumes: - redis_data:/data + restart: always networks: agenta-network: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index ff4dbdbac3..04563e7372 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -9,6 +9,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock networks: - agenta-network + restart: always backend: build: ./agenta-backend @@ -51,6 +52,7 @@ services: depends_on: mongo: condition: service_healthy + restart: always agenta-web: build: @@ -69,6 +71,7 @@ services: - "traefik.http.services.agenta-web.loadbalancer.server.port=3000" environment: - NEXT_PUBLIC_POSTHOG_API_KEY=phc_hmVSxIjTW1REBHXgj2aw4HW9X6CXb6FzerBgP9XenC7 + restart: always mongo: image: mongo:5.0 @@ -86,6 +89,7 @@ services: interval: 10s timeout: 10s retries: 20 + restart: always mongo_express: image: mongo-express @@ -100,6 +104,7 @@ services: depends_on: mongo: condition: service_healthy + restart: always redis: image: redis:latest @@ -107,6 +112,7 @@ services: - agenta-network volumes: - redis_data:/data + restart: always networks: agenta-network: From 1b4c02896d1db912325953f798b3870768f11634 Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Fri, 22 Dec 2023 15:55:33 +0100 Subject: [PATCH 123/267] add restart always local dev docker compose --- docker-compose.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 723707046c..f52ddd801b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock networks: - agenta-network + restart: always backend: build: ./agenta-backend @@ -54,6 +55,7 @@ services: depends_on: mongo: condition: service_healthy + restart: always agenta-web: build: @@ -73,6 +75,7 @@ services: - "traefik.http.services.agenta-web.loadbalancer.server.port=3000" environment: - NEXT_PUBLIC_POSTHOG_API_KEY=phc_hmVSxIjTW1REBHXgj2aw4HW9X6CXb6FzerBgP9XenC7 + restart: always mongo: image: mongo:5.0 @@ -90,6 +93,7 @@ services: interval: 10s timeout: 10s retries: 20 + restart: always mongo_express: image: mongo-express @@ -104,6 +108,7 @@ services: depends_on: mongo: condition: service_healthy + restart: always redis: image: redis:latest @@ -111,6 +116,7 @@ services: - agenta-network volumes: - redis_data:/data + restart: always networks: agenta-network: From 6f77e1122871cadc6724f47a60ff1518a358135e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:05:00 +0000 Subject: [PATCH 124/267] docs: update README.md [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba3770ee1a..87140fd14f 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ Check out our [Contributing Guide](https://docs.agenta.ai/contributing/getting-s ## Contributors ✨ -[![All Contributors](https://img.shields.io/badge/all_contributors-38-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-39-orange.svg?style=flat-square)](#contributors-) Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): @@ -270,6 +270,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d diego
diego

💻 brockWith
brockWith

💻 Dennis Zelada
Dennis Zelada

💻 + Romain Brucker
Romain Brucker

💻 From 3a7f3ec154be6d670fd6ea2efd0508c417b03bbb Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:05:01 +0000 Subject: [PATCH 125/267] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 5bd7afafd8..dcb1ee33c9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -363,6 +363,15 @@ "contributions": [ "code" ] + }, + { + "login": "romainrbr", + "name": "Romain Brucker", + "avatar_url": "https://avatars.githubusercontent.com/u/10381609?v=4", + "profile": "https://github.com/romainrbr", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, From 2589dd228ce5eeef5d6adcdf6742fcfa8f23f7c2 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Wed, 20 Dec 2023 13:36:02 +0100 Subject: [PATCH 126/267] Add tutorials and templates for text generation and summarization tasks --- docs/cookbook/list_templates_by_architecture.mdx | 6 ++++++ docs/cookbook/list_templates_by_use_case.mdx | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/cookbook/list_templates_by_architecture.mdx b/docs/cookbook/list_templates_by_architecture.mdx index 498839fdfd..3fe45724b6 100644 --- a/docs/cookbook/list_templates_by_architecture.mdx +++ b/docs/cookbook/list_templates_by_architecture.mdx @@ -5,6 +5,12 @@ description: "A collection of templates and tutorials indexed by architecture." This page is a work in progress. Please note that some of the entries are redundant. # Tutorials +## 📝 Text Generation +### [Single Prompt Application using OpenAI and Langchain](/tutorials/first-app-with-langchain) +Learn how to use our SDK to deploy an application with agenta. The application we will create uses OpenAI and Langchain. The application generates outreach messages in Linkedin to investors based on a startup name and idea. +### [Use Mistral from Huggingface for a Summarization Task](/tutorials/deploy-mistral-model) +Learn how to use a custom model with agenta. + # Templates ## ⛏️ Extraction diff --git a/docs/cookbook/list_templates_by_use_case.mdx b/docs/cookbook/list_templates_by_use_case.mdx index c9d7e955d9..3b8cc049fb 100644 --- a/docs/cookbook/list_templates_by_use_case.mdx +++ b/docs/cookbook/list_templates_by_use_case.mdx @@ -4,4 +4,10 @@ description: "A collection of templates and tutorials indexed by the the use cas --- This page is a work in progress. Please note that some of the entries are redundant. -## Human Ressources \ No newline at end of file +## Human Ressources +### [Extraction using OpenAI Functions and Langchain](/cookbook/extract_job_information) +Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. + +## Sales +### [Single Prompt Application using OpenAI and Langchain](/tutorials/first-app-with-langchain) +Learn how to use our SDK to deploy an application with agenta. The application we will create uses OpenAI and Langchain. The application generates outreach messages in Linkedin to investors based on a startup name and idea. From 6ae371c0722bcddcc801ace26def7aead323fa52 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 21 Dec 2023 17:46:24 +0100 Subject: [PATCH 127/267] Update title in deploy-mistral-model.mdx --- docs/tutorials/deploy-mistral-model.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/deploy-mistral-model.mdx b/docs/tutorials/deploy-mistral-model.mdx index 10ba7a131f..721de9b884 100644 --- a/docs/tutorials/deploy-mistral-model.mdx +++ b/docs/tutorials/deploy-mistral-model.mdx @@ -1,5 +1,5 @@ --- -title: Deploy Mistral-7B from Hugging Face +title: Use Mistral-7B from Hugging Face description: How to deploy an LLM application using Mistral-7B from Hugging Face' --- From c7a7895c580fa1485647bfc9d6ada50ebce39833 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 21 Dec 2023 17:46:30 +0100 Subject: [PATCH 128/267] Add RAG application tutorial and setup instructions --- docs/tutorials/build-rag-application.mdx | 77 ++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 docs/tutorials/build-rag-application.mdx diff --git a/docs/tutorials/build-rag-application.mdx b/docs/tutorials/build-rag-application.mdx new file mode 100644 index 0000000000..e19445c904 --- /dev/null +++ b/docs/tutorials/build-rag-application.mdx @@ -0,0 +1,77 @@ +--- +title: RAG application with LlamaIndex +description: Build a playground and evaluate with you RAG application +--- + +Retrieval Augmented Generation (RAG) is a very useful architecture for grounding the LLM application with your own knowldge base. However, it is not easy to build a robust RAG application that does not hallucinate and answers truthfully. + +In this tutorial, we will show how to use a RAG application built with [LlamaIndex](https://www.llamaindex.ai/). We will create a playground based on the RAG application allowing us to quickly test different configurations in a live playground. Then we will evaluate different variants of the RAG application with the playground. + +You can find the full code for this tutorial [here](https://github.com/Agenta-AI/qa_llama_index_playground) + +Let's get started + +## What are we building? + +Our goal is to build a RAG application. The application takes a transcript of a conversation and a question then returns the answer. + +We want to quickly iterate on the configuration of the RAG application and evaluate the performance of each configuration. + +Here is a list of parameters we would to experiment with in the playground: + +- How to split the transcript: the separator, the chunk size, and the overlap, and the text splitter to use in LlamaIndex (`TokenTextSplitter` or `SentenceSplitter`) +- The embedding model to be used (`Davinci`, `Curie`, `Babbage`, `ADA`, `Text_embed_ada_002`) +- The embedding mode: similarity mode or text search mode +- The LLM model to be used to generate the final response (`gpt3.5-turbo`, `gpt4`...) + +After finishing, we will have a playground where we can experiment with these different parameters live, and compare the outputs between different configuration side-by-side. + +In addition, we will be able to run evaluations on the different versions to score them, and later deploy the best version to production, without any overhead. + +## Installation and Setup + +First, let's make sure that you have the latest version of agenta installed. +```bash +pip install -U agenta +``` + +Now let's initialize our project + +```bash +agenta init +```` + +## Write the core application + +The idea behind agenta is to distangle the core application code from the parameters. So first let's write the core code of the application using some default parameters. Then we will extract the parameters, add them to the configuration and add the agenta lines of codes. + +### The core application + +Let's start by writing a simple application with LlamaIndex. + + +```python +text_splitter = TEXT_SPLITTERS[text_splitter]( + separator=ag.config.splitter_separator, + chunk_size=ag.config.text_splitter_chunk_size, + chunk_overlap=ag.config.text_splitter_chunk_overlap, +) +service_context = ServiceContext.from_defaults( + llm=OpenAI(temperature=ag.config.temperature, model=ag.config.model), + embed_model=OpenAIEmbedding( + mode=EMBEDDING_MODES[ag.config.embedding_mode], + model=EMBEDDING_MODELS[ag.config.embedding_model], + ), + node_parser=SimpleNodeParser(text_splitter=text_splitter), +) +# build a vector store index from the transcript as message documents +index = VectorStoreIndex.from_documents( + documents=[Document(text=transcript)], service_context=service_context +) + +query_engine = index.as_query_engine( + text_qa_template=prompt, service_context=service_context +) +response = query_engine.query(question) +print(response) +``` \ No newline at end of file From 8d8a8d60ffcc854d6bc2ba4c0995ebba60eadc70 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 21 Dec 2023 17:46:34 +0100 Subject: [PATCH 129/267] Add "build-rag-application" tutorial to mint.json --- docs/mint.json | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/mint.json b/docs/mint.json index a0457adb22..7b79f3ebed 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -76,6 +76,7 @@ "group": "Tutorials", "pages": [ "tutorials/first-app-with-langchain", + "tutorials/build-rag-application", "tutorials/deploy-mistral-model" ] }, From e0713f01a7f573a87cfdcc63fa6752afd56e711e Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Fri, 22 Dec 2023 22:19:35 +0100 Subject: [PATCH 130/267] Add RAG application tutorial with LlamaIndex --- docs/cookbook/list_templates_by_architecture.mdx | 3 +++ docs/cookbook/list_templates_by_technology.mdx | 4 ++++ docs/cookbook/list_templates_by_use_case.mdx | 2 ++ 3 files changed, 9 insertions(+) diff --git a/docs/cookbook/list_templates_by_architecture.mdx b/docs/cookbook/list_templates_by_architecture.mdx index 3fe45724b6..1adea1a975 100644 --- a/docs/cookbook/list_templates_by_architecture.mdx +++ b/docs/cookbook/list_templates_by_architecture.mdx @@ -11,6 +11,9 @@ Learn how to use our SDK to deploy an application with agenta. The application w ### [Use Mistral from Huggingface for a Summarization Task](/tutorials/deploy-mistral-model) Learn how to use a custom model with agenta. +## Retrieval Augmented Generation (RAG) +### [RAG Application with LlamaIndex](/tutorials/build-rag-application) +Learn how to create a RAG application with LlamaIndex and use it in agenta. You will create a playground in agenta where you can experiment with the parameters of the RAG application, test it and compare different versions. # Templates ## ⛏️ Extraction diff --git a/docs/cookbook/list_templates_by_technology.mdx b/docs/cookbook/list_templates_by_technology.mdx index 7548e4f873..bc7f38810f 100644 --- a/docs/cookbook/list_templates_by_technology.mdx +++ b/docs/cookbook/list_templates_by_technology.mdx @@ -9,6 +9,10 @@ description: "A collection of templates and tutorials indexed by the used framew ### [Extraction using OpenAI Functions and Langchain](/templates/extract_job_information) Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. +## LlamaIndex +### [RAG Application with LlamaIndex](/tutorials/build-rag-application) +Learn how to create a RAG application with LlamaIndex and use it in agenta. You will create a playground in agenta where you can experiment with the parameters of the RAG application, test it and compare different versions. The application takes a sales transcript and answers questions based on it. + ## OpenAI ### [Extraction using OpenAI Functions and Langchain](/templates/extract_job_information) diff --git a/docs/cookbook/list_templates_by_use_case.mdx b/docs/cookbook/list_templates_by_use_case.mdx index 3b8cc049fb..9fa5954236 100644 --- a/docs/cookbook/list_templates_by_use_case.mdx +++ b/docs/cookbook/list_templates_by_use_case.mdx @@ -11,3 +11,5 @@ Extracts job information (company name, job title, salary range) from a job desc ## Sales ### [Single Prompt Application using OpenAI and Langchain](/tutorials/first-app-with-langchain) Learn how to use our SDK to deploy an application with agenta. The application we will create uses OpenAI and Langchain. The application generates outreach messages in Linkedin to investors based on a startup name and idea. +### [RAG Application with LlamaIndex](/tutorials/build-rag-application) +Learn how to create a RAG application with LlamaIndex and use it in agenta. You will create a playground in agenta where you can experiment with the parameters of the RAG application, test it and compare different versions. The application takes a sales transcript and answers questions based on it. From a2888db8685bd8eca022b69daa4a6bf3466ff004 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Fri, 22 Dec 2023 22:19:41 +0100 Subject: [PATCH 131/267] Add tut_llama_index_1.png image to tutorial-rag-application --- .../tut_llama_index_1.png | Bin 0 -> 284361 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/tutorial-rag-application/tut_llama_index_1.png diff --git a/docs/images/tutorial-rag-application/tut_llama_index_1.png b/docs/images/tutorial-rag-application/tut_llama_index_1.png new file mode 100644 index 0000000000000000000000000000000000000000..66a33a70276df555b88e6f92f0b789a997546b8d GIT binary patch literal 284361 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≤~kjQfB)@G#|6 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

pvqT3czw&5VimZn&JHI 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 Date: Fri, 22 Dec 2023 22:19:53 +0100 Subject: [PATCH 132/267] added rag tutorial --- docs/tutorials/build-rag-application.mdx | 271 +++++++++++++++++++++-- 1 file changed, 251 insertions(+), 20 deletions(-) diff --git a/docs/tutorials/build-rag-application.mdx b/docs/tutorials/build-rag-application.mdx index e19445c904..307d1f6408 100644 --- a/docs/tutorials/build-rag-application.mdx +++ b/docs/tutorials/build-rag-application.mdx @@ -1,6 +1,6 @@ --- title: RAG application with LlamaIndex -description: Build a playground and evaluate with you RAG application +description: Build a playground to experiment and evaluate with you RAG application --- Retrieval Augmented Generation (RAG) is a very useful architecture for grounding the LLM application with your own knowldge base. However, it is not easy to build a robust RAG application that does not hallucinate and answers truthfully. @@ -51,27 +51,258 @@ Let's start by writing a simple application with LlamaIndex. ```python -text_splitter = TEXT_SPLITTERS[text_splitter]( - separator=ag.config.splitter_separator, - chunk_size=ag.config.text_splitter_chunk_size, - chunk_overlap=ag.config.text_splitter_chunk_overlap, +from llama_index import Document, ServiceContext, VectorStoreIndex +from llama_index.embeddings.openai import ( + OpenAIEmbedding, + OpenAIEmbeddingMode, + OpenAIEmbeddingModelType, ) -service_context = ServiceContext.from_defaults( - llm=OpenAI(temperature=ag.config.temperature, model=ag.config.model), - embed_model=OpenAIEmbedding( - mode=EMBEDDING_MODES[ag.config.embedding_mode], - model=EMBEDDING_MODELS[ag.config.embedding_model], - ), - node_parser=SimpleNodeParser(text_splitter=text_splitter), +from llama_index.langchain_helpers.text_splitter import ( + TokenTextSplitter, ) -# build a vector store index from the transcript as message documents -index = VectorStoreIndex.from_documents( - documents=[Document(text=transcript)], service_context=service_context +from llama_index.llms import OpenAI +from llama_index.text_splitter import TokenTextSplitter + + +def answer_qa(transcript: str, question: str): + text_splitter = TokenTextSplitter( + separator="\n", + chunk_size=1024, + chunk_overlap=20, + ) + service_context = ServiceContext.from_defaults( + llm=OpenAI(temperature=0.9, model="gpt-3.5-turbo"), + embed_model=OpenAIEmbedding( + mode=OpenAIEmbeddingMode.SIMILARITY_MODE, + model=OpenAIEmbeddingModelType.ADA, + ), + node_parser=text_splitter, + ) + # build a vector store index from the transcript as message documents + index = VectorStoreIndex.from_documents( + documents=[Document(text=transcript)], service_context=service_context + ) + + query_engine = index.as_query_engine( + service_context=service_context, response_mode="simple_summarize" + ) + + response = query_engine.query(question) + return response + + +if __name__ == "__main__": + with open("transcript", "r") as f: + transcript = f.read() + question = "What do they say about blackfriday?" + response = answer_qa(transcript, question) + print(response) +``` + +If you are not familiar with LlamaIndex, I encourage you to read the docs [here](https://docs.llamaindex.ai). + +However, here is a quick explanation of what is happening in the code above: + +```python + text_splitter = TokenTextSplitter( + separator="\n", + chunk_size=1024, + chunk_overlap=20, + ) + service_context = ServiceContext.from_defaults( + llm=OpenAI(temperature=0.9, model="gpt-3.5-turbo"), + embed_model=OpenAIEmbedding( + mode=OpenAIEmbeddingMode.SIMILARITY_MODE, + model=OpenAIEmbeddingModelType.ADA, + ), + node_parser=text_splitter, + ) + # build a vector store index from the transcript as message documents + index = VectorStoreIndex.from_documents( + documents=[Document(text=transcript)], service_context=service_context + ) +``` + +This part is responsible for ingesting the data and building the index. We specify how the input text should be split into chunks in the `text_splitter`, then which model to use for embedding and in the response in `service_context`. + +```python + query_engine = index.as_query_engine( + service_context=service_context, response_mode="simple_summarize" + ) + + response = query_engine.query(question) +``` + +This part is responsible for querying the index and generating the response. We specify the response mode to be `simple_summarize` which is one of the [response modes](https://docs.llamaindex.ai/en/stable/module_guides/deploying/query_engine/response_modes.html) in LlamaIndex. This response mode Truncates all text chunks to fit into a single LLM prompt. + +Basically, we are taking the transcript of the call, chunking it and embedding it, then later querying it using the simple_summarize technique, which first embeds the question, retrieve the most similar chunk, creates a prompt for it and summarize it using the LLM model. + + +## Make it into an agenta application + +Now that we have the core application, let's serve it to the agenta platform. In this first step we would not add the parameters yet, we will do that in the next step. We will just add it to agenta to be able to use it in the playground, evaluate it and deploy it. + +For this we need three things: +1. Modifying the code to initialize agenta and specify the entrypoint to the code (which will be converted to an endpoint) +2. Add a requirements.txt file +3. Adding the environment variables to a `.env` file + +### Modifying the code + +We just need to add the following lines to initialize agenta and specify the entrypoint to the code (which will be converted to an endpoint) + +```python +import agenta as ag + +ag.init() # This initializes agenta + +@ag.entrypoint() +def answer_qa(transcript: str, question: str): + # the rest of the code +``` + +`ag.init()` initializes agenta while `@ag.entrypoint()` is a wrapper around Fastapi that creates an entrypoint. + +### Adding a requirements.txt file + +We need to add a requirements.txt file to specify the dependencies of our application. In our case, we need to add `llama_index` and `agenta` to the requirements.txt file. + +```txt +llama_index +agenta +``` + +### Adding the environment variables to a `.env` file + +We need to add the environment variables to a `.env` file. In our case, we need to add the following variables: + +```bash +OPENAI_API_KEY= +```` + +### Serving the application to agenta +Finally we need serve the application to agenta. For this we need to run the following command: + +```bash +pip install -U agenta +agenta init +agenta variant serve app.py +``` + +`agenta init` initializes the llm application in the folder. It creates a `config.yaml` file that contains the configuration of the application. + +`agenta variant serve app.py` serves the application to agenta. It sends the code to the platform, which builds a docker image and deploy the endpoint. Additionally it is added to the UI. + +You should see the following outputs at success of the command: + +```bash +Congratulations! 🎉 +Your app has been deployed locally as an API. 🚀 You can access it here: https:////.lambda-url.eu-central-1.on.aws/ + +Read the API documentation. 📚 It's available at: https:////.lambda-url.eu-central-1.on.aws/docs + +Start experimenting with your app in the playground. 🎮 Go to: https://cloud.agenta.ai/apps//playground +``` + +Now you can jump to agenta and find a playground where you can interact with the application. + + + + + +# Adding parameters to the playground + +The version we have deployed to the playground does not have any parameters. We can test it, evaluate it, but we cannot modify it and test different configurations. + +Let's add a few parameters to the application to improve our playground and serve it again to agenta. + +To add a configuration to the application, we just need to register the default in the code after calling `agenta.init()`. When defining the parameters, we need to provide the type to render them correctly in the playground. + +```python +ag.config.register_default( + chunk_size=ag.intParam(1024, 256, 4096), + chunk_overlap=ag.intParam(20, 0, 100), + temperature=ag.intParam(0.9, 0.0, 1.0), + model=ag.MultipleChoiceParam( + "gpt-3.5-turbo", ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"]), + response_mode=ag.MultipleChoiceParam( + "simple_summarize", ["simple_summarize", "refine", "compact", "tree_summarize", "accumulate", "compact_accumulate"]), ) +``` -query_engine = index.as_query_engine( - text_qa_template=prompt, service_context=service_context +What we did here is to add the parameters, and specify the type of each parameter. `intParam` are integers with a default value, a minimum, maximum in that order. They are rendered as a slider in the playground. `MultipleChoiceParam` are multiple choice parameters with a default value and a list of choices. They are rendered as a dropdown in the playground. + +We chose here to select the most important parameters in a RAG. The chunk size, the chunk overlap, the temperature of the LLM model, the LLM model itself, and the response mode (you can see the [documentation of LlamaIndex](https://docs.llamaindex.ai/en/stable/module_guides/deploying/query_engine/response_modes.html) for more details about the response mode). + +To use the configuration in the code, you use the variable as `ag.config.` anywhere in the code. For instance: + +```python + text_splitter = TokenTextSplitter( + separator="\n", + chunk_size=ag.config.chunk_size, + chunk_overlap=ag.config.chunk_overlap, + ) +``` + +# Putting it all together + +Here is how our final code looks like: + +```python +import agenta as ag +from llama_index import Document, ServiceContext, VectorStoreIndex +from llama_index.embeddings.openai import ( + OpenAIEmbedding, + OpenAIEmbeddingMode, + OpenAIEmbeddingModelType, +) +from llama_index.langchain_helpers.text_splitter import ( + TokenTextSplitter, ) -response = query_engine.query(question) -print(response) -``` \ No newline at end of file +from llama_index.llms import OpenAI +from llama_index.text_splitter import TokenTextSplitter + +ag.init() +ag.config.default( + chunk_size=ag.IntParam(1024, 256, 4096), + chunk_overlap=ag.IntParam(20, 0, 100), + temperature=ag.IntParam(0.9, 0.0, 1.0), + model=ag.MultipleChoiceParam( + "gpt-3.5-turbo", ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"]), + response_mode=ag.MultipleChoiceParam( + "simple_summarize", ["simple_summarize", "refine", "compact", "tree_summarize", "accumulate", "compact_accumulate"]), +) + +@ag.entrypoint +def answer_qa(transcript: str, question: str): + text_splitter = TokenTextSplitter( + separator="\n", + chunk_size=ag.config.chunk_size, + chunk_overlap=ag.config.chunk_overlap, + ) + service_context = ServiceContext.from_defaults( + llm=OpenAI(temperature=ag.config.temperature, model=ag.config.model), + embed_model=OpenAIEmbedding( + mode=OpenAIEmbeddingMode.SIMILARITY_MODE, + model=OpenAIEmbeddingModelType.ADA, + ), + node_parser=text_splitter, + ) + # build a vector store index from the transcript as message documents + index = VectorStoreIndex.from_documents( + documents=[Document(text=transcript)], service_context=service_context + ) + + query_engine = index.as_query_engine( + service_context=service_context, response_mode=ag.config.response_mode + ) + + response = query_engine.query(question) + return response +``` + +Now let's serve it to agenta again: + +```bash +agenta variant serve app.py +``` From 71f5aadb66d3fd2c1bb1d3b7776f9003ee77aa59 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Fri, 22 Dec 2023 22:21:50 +0100 Subject: [PATCH 133/267] Update tutorial with link to full code --- docs/tutorials/build-rag-application.mdx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/build-rag-application.mdx b/docs/tutorials/build-rag-application.mdx index 307d1f6408..3061d12c5b 100644 --- a/docs/tutorials/build-rag-application.mdx +++ b/docs/tutorials/build-rag-application.mdx @@ -7,8 +7,9 @@ Retrieval Augmented Generation (RAG) is a very useful architecture for grounding In this tutorial, we will show how to use a RAG application built with [LlamaIndex](https://www.llamaindex.ai/). We will create a playground based on the RAG application allowing us to quickly test different configurations in a live playground. Then we will evaluate different variants of the RAG application with the playground. -You can find the full code for this tutorial [here](https://github.com/Agenta-AI/qa_llama_index_playground) - + +[You can find the full code for this tutorial here](https://github.com/Agenta-AI/qa_llama_index_playground) + Let's get started ## What are we building? @@ -306,3 +307,8 @@ Now let's serve it to agenta again: ```bash agenta variant serve app.py ``` + + +[You can find the full code for this tutorial here](https://github.com/Agenta-AI/qa_llama_index_playground) + + From b735d215c91a601ce5254488f66de879a1d71b51 Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 25 Dec 2023 08:06:40 +0100 Subject: [PATCH 134/267] fix `BaseSettings` PydanticImportError --- agenta-cli/agenta/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-cli/agenta/config.py b/agenta-cli/agenta/config.py index a072adf9a6..10e5e1965a 100644 --- a/agenta-cli/agenta/config.py +++ b/agenta-cli/agenta/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic.v1 import BaseSettings import os import toml From e2ff04d175336f7e1bd85a2a6e82820edcbe8641 Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 25 Dec 2023 08:07:12 +0100 Subject: [PATCH 135/267] fix / causing delete variant to fail in cloud --- agenta-cli/agenta/client/client.py | 526 +++++++++++++++++++++++++++++ 1 file changed, 526 insertions(+) create mode 100644 agenta-cli/agenta/client/client.py diff --git a/agenta-cli/agenta/client/client.py b/agenta-cli/agenta/client/client.py new file mode 100644 index 0000000000..1b9c4bcef3 --- /dev/null +++ b/agenta-cli/agenta/client/client.py @@ -0,0 +1,526 @@ +from typing import Dict, Any, Optional +import os +import time +import click +from pathlib import Path +from typing import List, Optional, Dict, Any + +import requests +from agenta.client.api_models import AppVariant, Image, VariantConfigPayload +from docker.models.images import Image as DockerImage +from requests.exceptions import RequestException + +BACKEND_URL_SUFFIX = os.environ.get("BACKEND_URL_SUFFIX", "api") + + +class APIRequestError(Exception): + """Exception to be raised when an API request fails.""" + + +def get_base_by_app_id_and_name( + app_id: str, base_name: str, host: str, api_key: str = None +) -> str: + """ + Get the base ID for a given app ID and base name. + + Args: + app_id (str): The ID of the app. + base_name (str): The name of the base. + host (str): The URL of the server. + api_key (str, optional): The API key to use for authentication. Defaults to None. + + Returns: + str: The ID of the base. + + Raises: + APIRequestError: If the request to get the base fails or the base does not exist on the server. + """ + response = requests.get( + f"{host}/{BACKEND_URL_SUFFIX}/bases/?app_id={app_id}&base_name={base_name}", + headers={"Authorization": api_key} if api_key is not None else None, + timeout=600, + ) + if response.status_code != 200: + error_message = response.json() + raise APIRequestError( + f"Request to get base failed with status code {response.status_code} and error message: {error_message}." + ) + if len(response.json()) == 0: + raise APIRequestError( + f"Base with name {base_name} does not exist on the server." + ) + else: + return response.json()[0]["base_id"] + + +def get_app_by_name(app_name: str, host: str, api_key: str = None) -> str: + """Get app by its name on the server. + + Args: + app_name (str): Name of the app + host (str): Hostname of the server + api_key (str): The API key to use for the request. + """ + + response = requests.get( + f"{host}/{BACKEND_URL_SUFFIX}/apps/?app_name={app_name}", + headers={"Authorization": api_key} if api_key is not None else None, + timeout=600, + ) + if response.status_code != 200: + error_message = response.json() + raise APIRequestError( + f"Request to get app failed with status code {response.status_code} and error message: {error_message}." + ) + if len(response.json()) == 0: + raise APIRequestError(f"App with name {app_name} does not exist on the server.") + else: + return response.json()[0]["app_id"] # only one app should exist for that name + + +def create_new_app(app_name: str, host: str, api_key: str = None) -> str: + """Creates new app on the server. + + Args: + app_name (str): Name of the app + host (str): Hostname of the server + api_key (str): The API key to use for the request. + """ + + response = requests.post( + f"{host}/{BACKEND_URL_SUFFIX}/apps/", + json={"app_name": app_name}, + headers={"Authorization": api_key} if api_key is not None else None, + timeout=600, + ) + if response.status_code != 200: + error_message = response.json() + raise APIRequestError( + f"Request to create new app failed with status code {response.status_code} and error message: {error_message}." + ) + return response.json()["app_id"] + + +def add_variant_to_server( + app_id: str, + base_name: str, + image: Image, + host: str, + api_key: str = None, + retries=10, + backoff_factor=1, +) -> Dict: + """ + Adds a variant to the server with a retry mechanism and a single-line loading state. + + Args: + app_id (str): The ID of the app to add the variant to. + base_name (str): The base name for the variant. + image (Image): The image to use for the variant. + host (str): The host URL of the server. + api_key (str): The API key to use for the request. + retries (int): Number of times to retry the request. + backoff_factor (float): Factor to determine the delay between retries (exponential backoff). + + Returns: + dict: The JSON response from the server. + + Raises: + APIRequestError: If the request to the server fails after retrying. + """ + variant_name = f"{base_name.lower()}.default" + payload = { + "variant_name": variant_name, + "base_name": base_name.lower(), + "config_name": "default", + "docker_id": image.docker_id, + "tags": image.tags, + } + + click.echo( + click.style("Waiting for the variant to be ready", fg="yellow"), nl=False + ) + + for attempt in range(retries): + try: + response = requests.post( + f"{host}/{BACKEND_URL_SUFFIX}/apps/{app_id}/variant/from-image/", + json=payload, + headers={"Authorization": api_key} if api_key is not None else None, + timeout=600, + ) + response.raise_for_status() + click.echo(click.style("\nVariant added successfully.", fg="green")) + return response.json() + except RequestException as e: + if attempt < retries - 1: + click.echo(click.style(".", fg="yellow"), nl=False) + time.sleep(backoff_factor * (2**attempt)) + else: + raise APIRequestError( + click.style( + f"\nRequest to app_variant endpoint failed with status code {response.status_code} and error message: {e}.", + fg="red", + ) + ) + except Exception as e: + raise APIRequestError( + click.style(f"\nAn unexpected error occurred: {e}", fg="red") + ) + + +def start_variant( + variant_id: str, + host: str, + env_vars: Optional[Dict[str, str]] = None, + api_key: str = None, +) -> str: + """ + Starts or stops a container with the given variant and exposes its endpoint. + + Args: + variant_id (str): The ID of the variant. + host (str): The host URL. + env_vars (Optional[Dict[str, str]]): Optional environment variables to inject into the container. + api_key (str): The API key to use for the request. + + Returns: + str: The endpoint of the container. + + Raises: + APIRequestError: If the API request fails. + """ + payload = {} + payload["action"] = {"action": "START"} + if env_vars: + payload["env_vars"] = env_vars + try: + response = requests.put( + f"{host}/{BACKEND_URL_SUFFIX}/variants/{variant_id}/", + json=payload, + headers={"Authorization": api_key} if api_key is not None else None, + timeout=600, + ) + if response.status_code == 404: + raise APIRequestError( + f"404: Variant with ID {variant_id} does not exist on the server." + ) + elif response.status_code != 200: + error_message = response.text + raise APIRequestError( + f"Request to start variant endpoint failed with status code {response.status_code} and error message: {error_message}." + ) + return response.json().get("uri", "") + + except RequestException as e: + raise APIRequestError(f"An error occurred while making the request: {e}") + + +def list_variants(app_id: str, host: str, api_key: str = None) -> List[AppVariant]: + """ + Returns a list of AppVariant objects for a given app_id and host. + + Args: + app_id (str): The ID of the app to retrieve variants for. + host (str): The URL of the host to make the request to. + api_key (str): The API key to use for the request. + + Returns: + List[AppVariant]: A list of AppVariant objects for the given app_id and host. + """ + response = requests.get( + f"{host}/{BACKEND_URL_SUFFIX}/apps/{app_id}/variants/", + headers={"Authorization": api_key} if api_key is not None else None, + timeout=600, + ) + # Check for successful request + if response.status_code == 403: + raise APIRequestError( + f"No app by id {app_id} exists or you do not have access to it." + ) + elif response.status_code == 404: + raise APIRequestError( + f"No app by id {app_id} exists or you do not have access to it." + ) + elif response.status_code != 200: + error_message = response.json() + raise APIRequestError( + f"Request to apps endpoint failed with status code {response.status_code} and error message: {error_message}." + ) + + app_variants = response.json() + return [AppVariant(**variant) for variant in app_variants] + + +def remove_variant(variant_id: str, host: str, api_key: str = None): + """ + Sends a DELETE request to the Agenta backend to remove a variant with the given ID. + + Args: + variant_id (str): The ID of the variant to be removed. + host (str): The URL of the Agenta backend. + api_key (str): The API key to use for the request. + + Raises: + APIRequestError: If the request to the remove_variant endpoint fails. + + Returns: + None + """ + response = requests.delete( + f"{host}/{BACKEND_URL_SUFFIX}/variants/{variant_id}/", + headers={ + "Content-Type": "application/json", + "Authorization": api_key if api_key is not None else None, + }, + timeout=600, + ) + + # Check for successful request + if response.status_code != 200: + error_message = response.json() + raise APIRequestError( + f"Request to remove_variant endpoint failed with status code {response.status_code} and error message: {error_message}" + ) + + +def update_variant_image(variant_id: str, image: Image, host: str, api_key: str = None): + """ + Update the image of a variant with the given ID. + + Args: + variant_id (str): The ID of the variant to update. + image (Image): The new image to set for the variant. + host (str): The URL of the host to send the request to. + api_key (str): The API key to use for the request. + + Raises: + APIRequestError: If the request to update the variant fails. + + Returns: + None + """ + response = requests.put( + f"{host}/{BACKEND_URL_SUFFIX}/variants/{variant_id}/image/", + json=image.dict(), + headers={"Authorization": api_key} if api_key is not None else None, + timeout=600, + ) + if response.status_code != 200: + error_message = response.json() + raise APIRequestError( + f"Request to update app_variant failed with status code {response.status_code} and error message: {error_message}." + ) + + +def send_docker_tar( + app_id: str, base_name: str, tar_path: Path, host: str, api_key: str = None +) -> Image: + """ + Sends a Docker tar file to the specified host to build an image for the given app ID and variant name. + + Args: + app_id (str): The ID of the app. + base_name (str): The name of the codebase. + tar_path (Path): The path to the Docker tar file. + host (str): The URL of the host to send the request to. + api_key (str): The API key to use for the request. + + Returns: + Image: The built Docker image. + + Raises: + Exception: If the response status code is 500, indicating that serving the variant failed. + """ + with tar_path.open("rb") as tar_file: + response = requests.post( + f"{host}/{BACKEND_URL_SUFFIX}/containers/build_image/?app_id={app_id}&base_name={base_name}", + files={ + "tar_file": tar_file, + }, + headers={"Authorization": api_key} if api_key is not None else None, + timeout=1200, + ) + + if response.status_code == 500: + response_error = response.json() + error_msg = "Serving the variant failed.\n" + error_msg += f"Log: {response_error}\n" + error_msg += "Here's how you may be able to solve the issue:\n" + error_msg += "- First, make sure that the requirements.txt file has all the dependencies that you need.\n" + error_msg += "- Second, check the Docker logs for the backend image to see the error when running the Docker container." + raise Exception(error_msg) + + response.raise_for_status() + image = Image.parse_obj(response.json()) + return image + + +def save_variant_config( + base_id: str, + config_name: str, + parameters: Dict[str, Any], + overwrite: bool, + host: str, + api_key: Optional[str] = None, +) -> None: + """ + Saves a variant configuration to the Agenta backend. + If the config already exists, it will be overwritten if the overwrite argument is set to True. + If the config does does not exist, a new variant will be created. + + Args: + base_id (str): The ID of the base configuration. + config_name (str): The name of the variant configuration. + parameters (Dict[str, Any]): The parameters of the variant configuration. + overwrite (bool): Whether to overwrite an existing variant configuration with the same name. + host (str): The URL of the Agenta backend. + api_key (Optional[str], optional): The API key to use for authentication. Defaults to None. + + Raises: + ValueError: If the 'host' argument is not specified. + APIRequestError: If the request to the Agenta backend fails. + + Returns: + None + """ + if host is None: + raise ValueError("The 'host' is not specified in save_variant_config") + + variant_config = VariantConfigPayload( + base_id=base_id, + config_name=config_name, + parameters=parameters, + overwrite=overwrite, + ) + try: + response = requests.post( + f"{host}/{BACKEND_URL_SUFFIX}/configs/", + json=variant_config.dict(), + headers={"Authorization": api_key} if api_key is not None else None, + timeout=600, + ) + request = f"POST {host}/{BACKEND_URL_SUFFIX}/configs/ {variant_config.dict()}" + # Check for successful request + if response.status_code != 200: + error_message = response.json().get("detail", "Unknown error") + raise APIRequestError( + f"Request {request} to save_variant_config endpoint failed with status code {response.status_code}. Error message: {error_message}" + ) + except RequestException as e: + raise APIRequestError(f"Request failed: {str(e)}") + + +def fetch_variant_config( + base_id: str, + host: str, + config_name: Optional[str] = None, + environment_name: Optional[str] = None, + api_key: Optional[str] = None, +) -> Dict[str, Any]: + """ + Fetch a variant configuration from the server. + + Args: + base_id (str): ID of the base configuration. + config_name (str): Configuration name. + environment_name (str): Name of the environment. + host (str): The server host URL. + api_key (Optional[str], optional): The API key to use for authentication. Defaults to None. + + Raises: + APIRequestError: If the API request fails. + + Returns: + dict: The requested variant configuration. + """ + + if host is None: + raise ValueError("The 'host' is not specified in fetch_variant_config") + + try: + if environment_name: + endpoint_params = f"?base_id={base_id}&environment_name={environment_name}" + elif config_name: + endpoint_params = f"?base_id={base_id}&config_name={config_name}" + else: + raise ValueError( + "Either 'config_name' or 'environment_name' must be specified in fetch_variant_config" + ) + response = requests.get( + f"{host}/{BACKEND_URL_SUFFIX}/configs/{endpoint_params}", + headers={"Authorization": api_key} if api_key is not None else None, + timeout=600, + ) + + request = f"GET {host}/{BACKEND_URL_SUFFIX}/configs/ {base_id} {config_name} {environment_name}" + + # Check for successful request + if response.status_code != 200: + error_message = response.json().get("detail", "Unknown error") + raise APIRequestError( + f"Request {request} to fetch_variant_config endpoint failed with status code {response.status_code}. Error message: {error_message}" + ) + + return response.json() + + except RequestException as e: + raise APIRequestError(f"Request failed: {str(e)}") + + +def validate_api_key(api_key: str, host: str) -> bool: + """ + Validates an API key with the Agenta backend. + + Args: + api_key (str): The API key to validate. + host (str): The URL of the Agenta backend. + + Returns: + bool: Whether the API key is valid or not. + """ + try: + headers = {"Authorization": api_key} + + prefix = api_key.split(".")[0] + + response = requests.get( + f"{host}/{BACKEND_URL_SUFFIX}/keys/{prefix}/validate/", + headers=headers, + timeout=600, + ) + if response.status_code != 200: + error_message = response.json() + raise APIRequestError( + f"Request to validate api key failed with status code {response.status_code} and error message: {error_message}." + ) + return True + except RequestException as e: + raise APIRequestError(f"An error occurred while making the request: {e}") + + +def retrieve_user_id(host: str, api_key: Optional[str] = None) -> str: + """Retrieve user ID from the server. + + Args: + host (str): The URL of the Agenta backend + api_key (str): The API key to validate with. + + Returns: + str: the user ID + """ + + try: + response = requests.get( + f"{host}/{BACKEND_URL_SUFFIX}/profile/", + headers={"Authorization": api_key} if api_key is not None else None, + timeout=600, + ) + if response.status_code != 200: + error_message = response.json().get("detail", "Unknown error") + raise APIRequestError( + f"Request to fetch_user_profile endpoint failed with status code {response.status_code}. Error message: {error_message}" + ) + return response.json()["id"] + except RequestException as e: + raise APIRequestError(f"Request failed: {str(e)}") From 77be6e51b05113ea21b74a857a2a214a15fc11d2 Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 25 Dec 2023 12:39:00 +0100 Subject: [PATCH 136/267] fix pydantic --- agenta-cli/agenta/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-cli/agenta/config.py b/agenta-cli/agenta/config.py index 10e5e1965a..a072adf9a6 100644 --- a/agenta-cli/agenta/config.py +++ b/agenta-cli/agenta/config.py @@ -1,4 +1,4 @@ -from pydantic.v1 import BaseSettings +from pydantic import BaseSettings import os import toml From 7bbc7f00326118c2c6af4a97ffad42e1e2f77d55 Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 25 Dec 2023 14:46:15 +0100 Subject: [PATCH 137/267] define function for openapi in cloud --- agenta-backend/agenta_backend/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/agenta-backend/agenta_backend/main.py b/agenta-backend/agenta_backend/main.py index c1c13e6b76..8b77910ce2 100644 --- a/agenta-backend/agenta_backend/main.py +++ b/agenta-backend/agenta_backend/main.py @@ -80,3 +80,7 @@ async def lifespan(application: FastAPI, cache=True): app.include_router(organization_router.router, prefix="/organizations") app.include_router(bases_router.router, prefix="/bases") app.include_router(configs_router.router, prefix="/configs") + +if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]: + import agenta_backend.cloud.main as cloud + app = cloud.extend_app_schema(app) \ No newline at end of file From 479c5b2772a2398eff452a46b09eda39e440b592 Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 25 Dec 2023 15:28:54 +0100 Subject: [PATCH 138/267] regenerate backend and set timeout to 60 --- agenta-cli/agenta/client/backend/__init__.py | 2 + agenta-cli/agenta/client/backend/client.py | 393 ++++++++----- .../agenta/client/backend/types/__init__.py | 2 + .../client/backend/types/invite_request.py | 36 ++ agenta-cli/agenta/client/client.py | 526 ------------------ 5 files changed, 281 insertions(+), 678 deletions(-) create mode 100644 agenta-cli/agenta/client/backend/types/invite_request.py delete mode 100644 agenta-cli/agenta/client/client.py diff --git a/agenta-cli/agenta/client/backend/__init__.py b/agenta-cli/agenta/client/backend/__init__.py index 05a11ef7da..eb6978b547 100644 --- a/agenta-cli/agenta/client/backend/__init__.py +++ b/agenta-cli/agenta/client/backend/__init__.py @@ -28,6 +28,7 @@ GetConfigReponse, HttpValidationError, Image, + InviteRequest, ListApiKeysOutput, NewTestset, Organization, @@ -75,6 +76,7 @@ "GetConfigReponse", "HttpValidationError", "Image", + "InviteRequest", "ListApiKeysOutput", "NewTestset", "Organization", diff --git a/agenta-cli/agenta/client/backend/client.py b/agenta-cli/agenta/client/backend/client.py index 33e3173cb3..61bf7c852b 100644 --- a/agenta-cli/agenta/client/backend/client.py +++ b/agenta-cli/agenta/client/backend/client.py @@ -39,6 +39,7 @@ from .types.get_config_reponse import GetConfigReponse from .types.http_validation_error import HttpValidationError from .types.image import Image +from .types.invite_request import InviteRequest from .types.list_api_keys_output import ListApiKeysOutput from .types.new_testset import NewTestset from .types.organization import Organization @@ -62,7 +63,7 @@ class AgentaApi: def __init__( - self, *, base_url: str, api_key: str, timeout: typing.Optional[float] = 600 + self, *, base_url: str, api_key: str, timeout: typing.Optional[float] = 60 ): self._client_wrapper = SyncClientWrapper( base_url=base_url, @@ -70,7 +71,7 @@ def __init__( httpx_client=httpx.Client(timeout=timeout), ) - def list_api_keys(self) -> ListApiKeysOutput: + def list_api_keys(self) -> typing.List[ListApiKeysOutput]: """ List all API keys associated with the authenticated user. @@ -79,15 +80,21 @@ def list_api_keys(self) -> ListApiKeysOutput: Returns: List[ListAPIKeysOutput]: A list of API Keys associated with the user. + + --- + from agenta.client import AgentaApi + + client = AgentaApi(api_key="YOUR_API_KEY", base_url="https://yourhost.com/path/to/api") + client.list_api_keys() """ _response = self._client_wrapper.httpx_client.request( "GET", urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "keys"), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: - return pydantic.parse_obj_as(ListApiKeysOutput, _response.json()) # type: ignore + return pydantic.parse_obj_as(typing.List[ListApiKeysOutput], _response.json()) # type: ignore try: _response_json = _response.json() except JSONDecodeError: @@ -108,7 +115,7 @@ def create_api_key(self) -> str: "POST", urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "keys"), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(str, _response.json()) # type: ignore @@ -146,7 +153,7 @@ def delete_api_key(self, key_prefix: str) -> typing.Dict[str, typing.Any]: f"{self._client_wrapper.get_base_url()}/", f"keys/{key_prefix}" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Dict[str, typing.Any], _response.json()) # type: ignore @@ -173,7 +180,7 @@ def validate_api_key(self, key_prefix: str) -> bool: f"{self._client_wrapper.get_base_url()}/", f"keys/{key_prefix}/validate" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(bool, _response.json()) # type: ignore @@ -205,7 +212,7 @@ def fetch_organization_details(self, org_id: str) -> typing.Any: f"{self._client_wrapper.get_base_url()}/", f"organizations_ee/{org_id}" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -217,7 +224,7 @@ def fetch_organization_details(self, org_id: str) -> typing.Any: raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def invite_to_org(self, org_id: str, *, email: str) -> typing.Any: + def invite_to_org(self, org_id: str, *, request: InviteRequest) -> typing.Any: """ Invite a user to an Organization. @@ -234,7 +241,7 @@ def invite_to_org(self, org_id: str, *, email: str) -> typing.Any: Parameters: - org_id: str. - - email: str. + - request: InviteRequest. """ _response = self._client_wrapper.httpx_client.request( "POST", @@ -242,9 +249,46 @@ def invite_to_org(self, org_id: str, *, email: str) -> typing.Any: f"{self._client_wrapper.get_base_url()}/", f"organizations_ee/{org_id}/invite", ), - json=jsonable_encoder({"email": email}), + json=jsonable_encoder(request), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) + if 200 <= _response.status_code < 300: + return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore + if _response.status_code == 422: + raise UnprocessableEntityError(pydantic.parse_obj_as(HttpValidationError, _response.json())) # type: ignore + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def resend_invitation(self, org_id: str, *, request: InviteRequest) -> typing.Any: + """ + Resend an invitation to a user to an Organization. + + Raises: + HTTPException: _description_; status_code: 500 + HTTPException: Invitation not found or has expired; status_code: 400 + HTTPException: You already belong to this organization; status_code: 400 + + Returns: + JSONResponse: Resent invitation to user; status_code: 200 + + Parameters: + - org_id: str. + + - request: InviteRequest. + """ + _response = self._client_wrapper.httpx_client.request( + "POST", + urllib.parse.urljoin( + f"{self._client_wrapper.get_base_url()}/", + f"organizations_ee/{org_id}/invite/resend", + ), + json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -281,7 +325,7 @@ def add_user_to_org(self, org_id: str, *, token: str) -> typing.Any: ), json=jsonable_encoder({"token": token}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -305,7 +349,7 @@ def create_organization(self, *, request: Organization) -> typing.Any: ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -345,7 +389,7 @@ def update_organization( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -362,7 +406,7 @@ def health_check(self) -> typing.Any: "GET", urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "health"), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -377,7 +421,7 @@ def user_profile(self) -> typing.Any: "GET", urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "profile"), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -412,7 +456,7 @@ def list_app_variants(self, app_id: str) -> typing.List[AppVariantOutput]: f"{self._client_wrapper.get_base_url()}/", f"apps/{app_id}/variants" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[AppVariantOutput], _response.json()) # type: ignore @@ -453,7 +497,7 @@ def get_variant_by_env(self, *, app_id: str, environment: str) -> AppVariantOutp {"app_id": app_id, "environment": environment} ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(AppVariantOutput, _response.json()) # type: ignore @@ -500,7 +544,7 @@ def list_apps( urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "apps"), params=remove_none_from_dict({"app_name": app_name, "org_id": org_id}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[App], _response.json()) # type: ignore @@ -541,7 +585,7 @@ def create_app( urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "apps"), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(CreateAppOutput, _response.json()) # type: ignore @@ -607,7 +651,7 @@ def add_variant_from_image( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -635,7 +679,7 @@ def remove_app(self, app_id: str) -> typing.Any: f"{self._client_wrapper.get_base_url()}/", f"apps/{app_id}" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -692,7 +736,7 @@ def create_app_and_variant_from_template( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(AppVariantOutput, _response.json()) # type: ignore @@ -729,7 +773,7 @@ def list_environments(self, app_id: str) -> typing.List[EnvironmentOutput]: f"{self._client_wrapper.get_base_url()}/", f"apps/{app_id}/environments" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[EnvironmentOutput], _response.json()) # type: ignore @@ -786,7 +830,7 @@ def add_variant_from_base_and_config( } ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(AddVariantFromBaseAndConfigResponse, _response.json()) # type: ignore @@ -837,7 +881,7 @@ def start_variant( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Uri, _response.json()) # type: ignore @@ -869,7 +913,7 @@ def remove_variant(self, variant_id: str) -> typing.Any: f"{self._client_wrapper.get_base_url()}/", f"variants/{variant_id}" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -911,7 +955,7 @@ def update_variant_parameters( ), json=jsonable_encoder({"parameters": parameters}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -950,7 +994,7 @@ def update_variant_image(self, variant_id: str, *, request: Image) -> typing.Any ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -987,7 +1031,7 @@ def fetch_list_evaluations(self, *, app_id: str) -> typing.List[Evaluation]: ), params=remove_none_from_dict({"app_id": app_id}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[Evaluation], _response.json()) # type: ignore @@ -1049,7 +1093,7 @@ def create_evaluation( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(SimpleEvaluationOutput, _response.json()) # type: ignore @@ -1088,7 +1132,7 @@ def delete_evaluations( ), json=jsonable_encoder({"evaluations_ids": evaluations_ids}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[str], _response.json()) # type: ignore @@ -1120,7 +1164,7 @@ def fetch_evaluation(self, evaluation_id: str) -> Evaluation: f"evaluations/{evaluation_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Evaluation, _response.json()) # type: ignore @@ -1168,7 +1212,7 @@ def update_evaluation( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -1210,7 +1254,7 @@ def fetch_evaluation_scenarios( f"evaluations/{evaluation_id}/evaluation_scenarios", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[EvaluationScenario], _response.json()) # type: ignore @@ -1247,7 +1291,7 @@ def create_evaluation_scenario( ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -1326,7 +1370,7 @@ def update_evaluation_scenario( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -1393,7 +1437,7 @@ def evaluate_ai_critique( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(str, _response.json()) # type: ignore @@ -1433,7 +1477,7 @@ def get_evaluation_scenario_score( f"evaluations/evaluation_scenario/{evaluation_scenario_id}/score", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Dict[str, str], _response.json()) # type: ignore @@ -1470,7 +1514,7 @@ def update_evaluation_scenario_score( ), json=jsonable_encoder({"score": score}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -1502,7 +1546,7 @@ def fetch_results(self, evaluation_id: str) -> typing.Any: f"evaluations/{evaluation_id}/results", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -1535,7 +1579,7 @@ def create_custom_evaluation( ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -1567,7 +1611,7 @@ def get_custom_evaluation(self, id: str) -> CustomEvaluationDetail: f"evaluations/custom_evaluation/{id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(CustomEvaluationDetail, _response.json()) # type: ignore @@ -1602,7 +1646,7 @@ def update_custom_evaluation( ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -1641,7 +1685,7 @@ def list_custom_evaluations( f"evaluations/custom_evaluation/list/{app_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[CustomEvaluationOutput], _response.json()) # type: ignore @@ -1680,7 +1724,7 @@ def get_custom_evaluation_names( f"evaluations/custom_evaluation/{app_name}/names", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[CustomEvaluationNames], _response.json()) # type: ignore @@ -1741,7 +1785,7 @@ def execute_custom_evaluation( } ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -1767,7 +1811,7 @@ def webhook_example_fake(self) -> EvaluationWebhook: "evaluations/webhook_example_fake", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(EvaluationWebhook, _response.json()) # type: ignore @@ -1814,7 +1858,7 @@ def upload_file( ), files={"file": file}, headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(TestSetSimpleResponse, _response.json()) # type: ignore @@ -1843,7 +1887,7 @@ def import_testset(self) -> TestSetSimpleResponse: f"{self._client_wrapper.get_base_url()}/", "testsets/endpoint" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(TestSetSimpleResponse, _response.json()) # type: ignore @@ -1881,7 +1925,7 @@ def create_testset( ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(TestSetSimpleResponse, _response.json()) # type: ignore @@ -1912,7 +1956,7 @@ def get_single_testset(self, testset_id: str) -> typing.Any: f"{self._client_wrapper.get_base_url()}/", f"testsets/{testset_id}" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -1947,7 +1991,7 @@ def update_testset(self, testset_id: str, *, request: NewTestset) -> typing.Any: ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -1984,7 +2028,7 @@ def get_testsets(self, *, app_id: str) -> typing.List[TestSetOutputResponse]: urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "testsets"), params=remove_none_from_dict({"app_id": app_id}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[TestSetOutputResponse], _response.json()) # type: ignore @@ -2019,7 +2063,7 @@ def delete_testsets(self, *, testset_ids: typing.List[str]) -> typing.List[str]: urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "testsets"), json=jsonable_encoder({"testset_ids": testset_ids}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[str], _response.json()) # type: ignore @@ -2060,7 +2104,7 @@ def build_image(self, *, app_id: str, base_name: str, tar_file: typing.IO) -> Im data=jsonable_encoder({}), files={"tar_file": tar_file}, headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Image, _response.json()) # type: ignore @@ -2090,7 +2134,7 @@ def restart_container(self, *, variant_id: str) -> typing.Dict[str, typing.Any]: ), json=jsonable_encoder({"variant_id": variant_id}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Dict[str, typing.Any], _response.json()) # type: ignore @@ -2119,7 +2163,7 @@ def container_templates(self) -> ContainerTemplatesResponse: f"{self._client_wrapper.get_base_url()}/", "containers/templates" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ContainerTemplatesResponse, _response.json()) # type: ignore @@ -2163,7 +2207,7 @@ def construct_app_container_url( {"base_id": base_id, "variant_id": variant_id} ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Uri, _response.json()) # type: ignore @@ -2203,7 +2247,7 @@ def deploy_to_environment( {"environment_name": environment_name, "variant_id": variant_id} ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -2275,7 +2319,7 @@ def create_trace( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(str, _response.json()) # type: ignore @@ -2306,7 +2350,7 @@ def get_traces(self, app_id: str, variant_id: str) -> typing.List[Trace]: f"observability/traces/{app_id}/{variant_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[Trace], _response.json()) # type: ignore @@ -2330,7 +2374,7 @@ def get_single_trace(self, trace_id: str) -> Trace: f"observability/traces/{trace_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Trace, _response.json()) # type: ignore @@ -2357,7 +2401,7 @@ def update_trace_status(self, trace_id: str, *, status: str) -> bool: ), json=jsonable_encoder({"status": status}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(bool, _response.json()) # type: ignore @@ -2460,7 +2504,7 @@ def create_span( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(str, _response.json()) # type: ignore @@ -2489,7 +2533,7 @@ def get_spans_of_trace(self, trace_id: str) -> typing.List[Span]: f"observability/spans/{trace_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[Span], _response.json()) # type: ignore @@ -2518,7 +2562,7 @@ def get_feedbacks(self, trace_id: str) -> typing.List[Feedback]: f"observability/feedbacks/{trace_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[Feedback], _response.json()) # type: ignore @@ -2563,7 +2607,7 @@ def create_feedback( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(str, _response.json()) # type: ignore @@ -2589,7 +2633,7 @@ def get_feedback(self, trace_id: str, feedback_id: str) -> Feedback: f"observability/feedbacks/{trace_id}/{feedback_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Feedback, _response.json()) # type: ignore @@ -2635,7 +2679,7 @@ def update_feedback( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Feedback, _response.json()) # type: ignore @@ -2672,7 +2716,7 @@ def list_organizations(self) -> typing.List[Organization]: f"{self._client_wrapper.get_base_url()}/", "organizations" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[Organization], _response.json()) # type: ignore @@ -2689,7 +2733,7 @@ def get_own_org(self) -> OrganizationOutput: f"{self._client_wrapper.get_base_url()}/", "organizations/own" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(OrganizationOutput, _response.json()) # type: ignore @@ -2734,7 +2778,7 @@ def list_bases( urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "bases"), params=remove_none_from_dict({"app_id": app_id, "base_name": base_name}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[BaseOutput], _response.json()) # type: ignore @@ -2772,7 +2816,7 @@ def get_config( } ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(GetConfigReponse, _response.json()) # type: ignore @@ -2814,7 +2858,7 @@ def save_config( } ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -2829,7 +2873,7 @@ def save_config( class AsyncAgentaApi: def __init__( - self, *, base_url: str, api_key: str, timeout: typing.Optional[float] = 600 + self, *, base_url: str, api_key: str, timeout: typing.Optional[float] = 60 ): self._client_wrapper = AsyncClientWrapper( base_url=base_url, @@ -2837,7 +2881,7 @@ def __init__( httpx_client=httpx.AsyncClient(timeout=timeout), ) - async def list_api_keys(self) -> ListApiKeysOutput: + async def list_api_keys(self) -> typing.List[ListApiKeysOutput]: """ List all API keys associated with the authenticated user. @@ -2846,15 +2890,21 @@ async def list_api_keys(self) -> ListApiKeysOutput: Returns: List[ListAPIKeysOutput]: A list of API Keys associated with the user. + + --- + from agenta.client import AsyncAgentaApi + + client = AsyncAgentaApi(api_key="YOUR_API_KEY", base_url="https://yourhost.com/path/to/api") + await client.list_api_keys() """ _response = await self._client_wrapper.httpx_client.request( "GET", urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "keys"), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: - return pydantic.parse_obj_as(ListApiKeysOutput, _response.json()) # type: ignore + return pydantic.parse_obj_as(typing.List[ListApiKeysOutput], _response.json()) # type: ignore try: _response_json = _response.json() except JSONDecodeError: @@ -2875,7 +2925,7 @@ async def create_api_key(self) -> str: "POST", urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "keys"), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(str, _response.json()) # type: ignore @@ -2913,7 +2963,7 @@ async def delete_api_key(self, key_prefix: str) -> typing.Dict[str, typing.Any]: f"{self._client_wrapper.get_base_url()}/", f"keys/{key_prefix}" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Dict[str, typing.Any], _response.json()) # type: ignore @@ -2940,7 +2990,7 @@ async def validate_api_key(self, key_prefix: str) -> bool: f"{self._client_wrapper.get_base_url()}/", f"keys/{key_prefix}/validate" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(bool, _response.json()) # type: ignore @@ -2972,7 +3022,7 @@ async def fetch_organization_details(self, org_id: str) -> typing.Any: f"{self._client_wrapper.get_base_url()}/", f"organizations_ee/{org_id}" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -2984,7 +3034,7 @@ async def fetch_organization_details(self, org_id: str) -> typing.Any: raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def invite_to_org(self, org_id: str, *, email: str) -> typing.Any: + async def invite_to_org(self, org_id: str, *, request: InviteRequest) -> typing.Any: """ Invite a user to an Organization. @@ -3001,7 +3051,7 @@ async def invite_to_org(self, org_id: str, *, email: str) -> typing.Any: Parameters: - org_id: str. - - email: str. + - request: InviteRequest. """ _response = await self._client_wrapper.httpx_client.request( "POST", @@ -3009,9 +3059,48 @@ async def invite_to_org(self, org_id: str, *, email: str) -> typing.Any: f"{self._client_wrapper.get_base_url()}/", f"organizations_ee/{org_id}/invite", ), - json=jsonable_encoder({"email": email}), + json=jsonable_encoder(request), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) + if 200 <= _response.status_code < 300: + return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore + if _response.status_code == 422: + raise UnprocessableEntityError(pydantic.parse_obj_as(HttpValidationError, _response.json())) # type: ignore + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def resend_invitation( + self, org_id: str, *, request: InviteRequest + ) -> typing.Any: + """ + Resend an invitation to a user to an Organization. + + Raises: + HTTPException: _description_; status_code: 500 + HTTPException: Invitation not found or has expired; status_code: 400 + HTTPException: You already belong to this organization; status_code: 400 + + Returns: + JSONResponse: Resent invitation to user; status_code: 200 + + Parameters: + - org_id: str. + + - request: InviteRequest. + """ + _response = await self._client_wrapper.httpx_client.request( + "POST", + urllib.parse.urljoin( + f"{self._client_wrapper.get_base_url()}/", + f"organizations_ee/{org_id}/invite/resend", + ), + json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3048,7 +3137,7 @@ async def add_user_to_org(self, org_id: str, *, token: str) -> typing.Any: ), json=jsonable_encoder({"token": token}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3072,7 +3161,7 @@ async def create_organization(self, *, request: Organization) -> typing.Any: ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3112,7 +3201,7 @@ async def update_organization( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3129,7 +3218,7 @@ async def health_check(self) -> typing.Any: "GET", urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "health"), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3144,7 +3233,7 @@ async def user_profile(self) -> typing.Any: "GET", urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "profile"), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3179,7 +3268,7 @@ async def list_app_variants(self, app_id: str) -> typing.List[AppVariantOutput]: f"{self._client_wrapper.get_base_url()}/", f"apps/{app_id}/variants" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[AppVariantOutput], _response.json()) # type: ignore @@ -3222,7 +3311,7 @@ async def get_variant_by_env( {"app_id": app_id, "environment": environment} ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(AppVariantOutput, _response.json()) # type: ignore @@ -3269,7 +3358,7 @@ async def list_apps( urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "apps"), params=remove_none_from_dict({"app_name": app_name, "org_id": org_id}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[App], _response.json()) # type: ignore @@ -3310,7 +3399,7 @@ async def create_app( urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "apps"), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(CreateAppOutput, _response.json()) # type: ignore @@ -3376,7 +3465,7 @@ async def add_variant_from_image( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3404,7 +3493,7 @@ async def remove_app(self, app_id: str) -> typing.Any: f"{self._client_wrapper.get_base_url()}/", f"apps/{app_id}" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3461,7 +3550,7 @@ async def create_app_and_variant_from_template( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(AppVariantOutput, _response.json()) # type: ignore @@ -3498,7 +3587,7 @@ async def list_environments(self, app_id: str) -> typing.List[EnvironmentOutput] f"{self._client_wrapper.get_base_url()}/", f"apps/{app_id}/environments" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[EnvironmentOutput], _response.json()) # type: ignore @@ -3555,7 +3644,7 @@ async def add_variant_from_base_and_config( } ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(AddVariantFromBaseAndConfigResponse, _response.json()) # type: ignore @@ -3606,7 +3695,7 @@ async def start_variant( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Uri, _response.json()) # type: ignore @@ -3638,7 +3727,7 @@ async def remove_variant(self, variant_id: str) -> typing.Any: f"{self._client_wrapper.get_base_url()}/", f"variants/{variant_id}" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3680,7 +3769,7 @@ async def update_variant_parameters( ), json=jsonable_encoder({"parameters": parameters}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3721,7 +3810,7 @@ async def update_variant_image( ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3758,7 +3847,7 @@ async def fetch_list_evaluations(self, *, app_id: str) -> typing.List[Evaluation ), params=remove_none_from_dict({"app_id": app_id}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[Evaluation], _response.json()) # type: ignore @@ -3820,7 +3909,7 @@ async def create_evaluation( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(SimpleEvaluationOutput, _response.json()) # type: ignore @@ -3859,7 +3948,7 @@ async def delete_evaluations( ), json=jsonable_encoder({"evaluations_ids": evaluations_ids}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[str], _response.json()) # type: ignore @@ -3891,7 +3980,7 @@ async def fetch_evaluation(self, evaluation_id: str) -> Evaluation: f"evaluations/{evaluation_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Evaluation, _response.json()) # type: ignore @@ -3939,7 +4028,7 @@ async def update_evaluation( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -3981,7 +4070,7 @@ async def fetch_evaluation_scenarios( f"evaluations/{evaluation_id}/evaluation_scenarios", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[EvaluationScenario], _response.json()) # type: ignore @@ -4018,7 +4107,7 @@ async def create_evaluation_scenario( ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -4097,7 +4186,7 @@ async def update_evaluation_scenario( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -4164,7 +4253,7 @@ async def evaluate_ai_critique( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(str, _response.json()) # type: ignore @@ -4204,7 +4293,7 @@ async def get_evaluation_scenario_score( f"evaluations/evaluation_scenario/{evaluation_scenario_id}/score", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Dict[str, str], _response.json()) # type: ignore @@ -4241,7 +4330,7 @@ async def update_evaluation_scenario_score( ), json=jsonable_encoder({"score": score}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -4273,7 +4362,7 @@ async def fetch_results(self, evaluation_id: str) -> typing.Any: f"evaluations/{evaluation_id}/results", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -4306,7 +4395,7 @@ async def create_custom_evaluation( ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -4338,7 +4427,7 @@ async def get_custom_evaluation(self, id: str) -> CustomEvaluationDetail: f"evaluations/custom_evaluation/{id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(CustomEvaluationDetail, _response.json()) # type: ignore @@ -4373,7 +4462,7 @@ async def update_custom_evaluation( ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -4412,7 +4501,7 @@ async def list_custom_evaluations( f"evaluations/custom_evaluation/list/{app_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[CustomEvaluationOutput], _response.json()) # type: ignore @@ -4451,7 +4540,7 @@ async def get_custom_evaluation_names( f"evaluations/custom_evaluation/{app_name}/names", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[CustomEvaluationNames], _response.json()) # type: ignore @@ -4512,7 +4601,7 @@ async def execute_custom_evaluation( } ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -4538,7 +4627,7 @@ async def webhook_example_fake(self) -> EvaluationWebhook: "evaluations/webhook_example_fake", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(EvaluationWebhook, _response.json()) # type: ignore @@ -4585,7 +4674,7 @@ async def upload_file( ), files={"file": file}, headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(TestSetSimpleResponse, _response.json()) # type: ignore @@ -4614,7 +4703,7 @@ async def import_testset(self) -> TestSetSimpleResponse: f"{self._client_wrapper.get_base_url()}/", "testsets/endpoint" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(TestSetSimpleResponse, _response.json()) # type: ignore @@ -4652,7 +4741,7 @@ async def create_testset( ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(TestSetSimpleResponse, _response.json()) # type: ignore @@ -4683,7 +4772,7 @@ async def get_single_testset(self, testset_id: str) -> typing.Any: f"{self._client_wrapper.get_base_url()}/", f"testsets/{testset_id}" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -4720,7 +4809,7 @@ async def update_testset( ), json=jsonable_encoder(request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -4757,7 +4846,7 @@ async def get_testsets(self, *, app_id: str) -> typing.List[TestSetOutputRespons urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "testsets"), params=remove_none_from_dict({"app_id": app_id}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[TestSetOutputResponse], _response.json()) # type: ignore @@ -4794,7 +4883,7 @@ async def delete_testsets( urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "testsets"), json=jsonable_encoder({"testset_ids": testset_ids}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[str], _response.json()) # type: ignore @@ -4837,7 +4926,7 @@ async def build_image( data=jsonable_encoder({}), files={"tar_file": tar_file}, headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Image, _response.json()) # type: ignore @@ -4869,7 +4958,7 @@ async def restart_container( ), json=jsonable_encoder({"variant_id": variant_id}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Dict[str, typing.Any], _response.json()) # type: ignore @@ -4898,7 +4987,7 @@ async def container_templates(self) -> ContainerTemplatesResponse: f"{self._client_wrapper.get_base_url()}/", "containers/templates" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ContainerTemplatesResponse, _response.json()) # type: ignore @@ -4942,7 +5031,7 @@ async def construct_app_container_url( {"base_id": base_id, "variant_id": variant_id} ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Uri, _response.json()) # type: ignore @@ -4982,7 +5071,7 @@ async def deploy_to_environment( {"environment_name": environment_name, "variant_id": variant_id} ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore @@ -5054,7 +5143,7 @@ async def create_trace( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(str, _response.json()) # type: ignore @@ -5085,7 +5174,7 @@ async def get_traces(self, app_id: str, variant_id: str) -> typing.List[Trace]: f"observability/traces/{app_id}/{variant_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[Trace], _response.json()) # type: ignore @@ -5109,7 +5198,7 @@ async def get_single_trace(self, trace_id: str) -> Trace: f"observability/traces/{trace_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Trace, _response.json()) # type: ignore @@ -5136,7 +5225,7 @@ async def update_trace_status(self, trace_id: str, *, status: str) -> bool: ), json=jsonable_encoder({"status": status}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(bool, _response.json()) # type: ignore @@ -5239,7 +5328,7 @@ async def create_span( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(str, _response.json()) # type: ignore @@ -5268,7 +5357,7 @@ async def get_spans_of_trace(self, trace_id: str) -> typing.List[Span]: f"observability/spans/{trace_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[Span], _response.json()) # type: ignore @@ -5297,7 +5386,7 @@ async def get_feedbacks(self, trace_id: str) -> typing.List[Feedback]: f"observability/feedbacks/{trace_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[Feedback], _response.json()) # type: ignore @@ -5342,7 +5431,7 @@ async def create_feedback( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(str, _response.json()) # type: ignore @@ -5368,7 +5457,7 @@ async def get_feedback(self, trace_id: str, feedback_id: str) -> Feedback: f"observability/feedbacks/{trace_id}/{feedback_id}", ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Feedback, _response.json()) # type: ignore @@ -5414,7 +5503,7 @@ async def update_feedback( ), json=jsonable_encoder(_request), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Feedback, _response.json()) # type: ignore @@ -5451,7 +5540,7 @@ async def list_organizations(self) -> typing.List[Organization]: f"{self._client_wrapper.get_base_url()}/", "organizations" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[Organization], _response.json()) # type: ignore @@ -5468,7 +5557,7 @@ async def get_own_org(self) -> OrganizationOutput: f"{self._client_wrapper.get_base_url()}/", "organizations/own" ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(OrganizationOutput, _response.json()) # type: ignore @@ -5513,7 +5602,7 @@ async def list_bases( urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "bases"), params=remove_none_from_dict({"app_id": app_id, "base_name": base_name}), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[BaseOutput], _response.json()) # type: ignore @@ -5551,7 +5640,7 @@ async def get_config( } ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(GetConfigReponse, _response.json()) # type: ignore @@ -5593,7 +5682,7 @@ async def save_config( } ), headers=self._client_wrapper.get_headers(), - timeout=600, + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.Any, _response.json()) # type: ignore diff --git a/agenta-cli/agenta/client/backend/types/__init__.py b/agenta-cli/agenta/client/backend/types/__init__.py index 9af97905b1..4be042f7a1 100644 --- a/agenta-cli/agenta/client/backend/types/__init__.py +++ b/agenta-cli/agenta/client/backend/types/__init__.py @@ -29,6 +29,7 @@ from .get_config_reponse import GetConfigReponse from .http_validation_error import HttpValidationError from .image import Image +from .invite_request import InviteRequest from .list_api_keys_output import ListApiKeysOutput from .new_testset import NewTestset from .organization import Organization @@ -74,6 +75,7 @@ "GetConfigReponse", "HttpValidationError", "Image", + "InviteRequest", "ListApiKeysOutput", "NewTestset", "Organization", diff --git a/agenta-cli/agenta/client/backend/types/invite_request.py b/agenta-cli/agenta/client/backend/types/invite_request.py new file mode 100644 index 0000000000..38a759ad10 --- /dev/null +++ b/agenta-cli/agenta/client/backend/types/invite_request.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime + +try: + import pydantic.v1 as pydantic # type: ignore +except ImportError: + import pydantic # type: ignore + + +class InviteRequest(pydantic.BaseModel): + email: str + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults: typing.Any = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + return super().dict(**kwargs_with_defaults) + + class Config: + frozen = True + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} diff --git a/agenta-cli/agenta/client/client.py b/agenta-cli/agenta/client/client.py deleted file mode 100644 index 1b9c4bcef3..0000000000 --- a/agenta-cli/agenta/client/client.py +++ /dev/null @@ -1,526 +0,0 @@ -from typing import Dict, Any, Optional -import os -import time -import click -from pathlib import Path -from typing import List, Optional, Dict, Any - -import requests -from agenta.client.api_models import AppVariant, Image, VariantConfigPayload -from docker.models.images import Image as DockerImage -from requests.exceptions import RequestException - -BACKEND_URL_SUFFIX = os.environ.get("BACKEND_URL_SUFFIX", "api") - - -class APIRequestError(Exception): - """Exception to be raised when an API request fails.""" - - -def get_base_by_app_id_and_name( - app_id: str, base_name: str, host: str, api_key: str = None -) -> str: - """ - Get the base ID for a given app ID and base name. - - Args: - app_id (str): The ID of the app. - base_name (str): The name of the base. - host (str): The URL of the server. - api_key (str, optional): The API key to use for authentication. Defaults to None. - - Returns: - str: The ID of the base. - - Raises: - APIRequestError: If the request to get the base fails or the base does not exist on the server. - """ - response = requests.get( - f"{host}/{BACKEND_URL_SUFFIX}/bases/?app_id={app_id}&base_name={base_name}", - headers={"Authorization": api_key} if api_key is not None else None, - timeout=600, - ) - if response.status_code != 200: - error_message = response.json() - raise APIRequestError( - f"Request to get base failed with status code {response.status_code} and error message: {error_message}." - ) - if len(response.json()) == 0: - raise APIRequestError( - f"Base with name {base_name} does not exist on the server." - ) - else: - return response.json()[0]["base_id"] - - -def get_app_by_name(app_name: str, host: str, api_key: str = None) -> str: - """Get app by its name on the server. - - Args: - app_name (str): Name of the app - host (str): Hostname of the server - api_key (str): The API key to use for the request. - """ - - response = requests.get( - f"{host}/{BACKEND_URL_SUFFIX}/apps/?app_name={app_name}", - headers={"Authorization": api_key} if api_key is not None else None, - timeout=600, - ) - if response.status_code != 200: - error_message = response.json() - raise APIRequestError( - f"Request to get app failed with status code {response.status_code} and error message: {error_message}." - ) - if len(response.json()) == 0: - raise APIRequestError(f"App with name {app_name} does not exist on the server.") - else: - return response.json()[0]["app_id"] # only one app should exist for that name - - -def create_new_app(app_name: str, host: str, api_key: str = None) -> str: - """Creates new app on the server. - - Args: - app_name (str): Name of the app - host (str): Hostname of the server - api_key (str): The API key to use for the request. - """ - - response = requests.post( - f"{host}/{BACKEND_URL_SUFFIX}/apps/", - json={"app_name": app_name}, - headers={"Authorization": api_key} if api_key is not None else None, - timeout=600, - ) - if response.status_code != 200: - error_message = response.json() - raise APIRequestError( - f"Request to create new app failed with status code {response.status_code} and error message: {error_message}." - ) - return response.json()["app_id"] - - -def add_variant_to_server( - app_id: str, - base_name: str, - image: Image, - host: str, - api_key: str = None, - retries=10, - backoff_factor=1, -) -> Dict: - """ - Adds a variant to the server with a retry mechanism and a single-line loading state. - - Args: - app_id (str): The ID of the app to add the variant to. - base_name (str): The base name for the variant. - image (Image): The image to use for the variant. - host (str): The host URL of the server. - api_key (str): The API key to use for the request. - retries (int): Number of times to retry the request. - backoff_factor (float): Factor to determine the delay between retries (exponential backoff). - - Returns: - dict: The JSON response from the server. - - Raises: - APIRequestError: If the request to the server fails after retrying. - """ - variant_name = f"{base_name.lower()}.default" - payload = { - "variant_name": variant_name, - "base_name": base_name.lower(), - "config_name": "default", - "docker_id": image.docker_id, - "tags": image.tags, - } - - click.echo( - click.style("Waiting for the variant to be ready", fg="yellow"), nl=False - ) - - for attempt in range(retries): - try: - response = requests.post( - f"{host}/{BACKEND_URL_SUFFIX}/apps/{app_id}/variant/from-image/", - json=payload, - headers={"Authorization": api_key} if api_key is not None else None, - timeout=600, - ) - response.raise_for_status() - click.echo(click.style("\nVariant added successfully.", fg="green")) - return response.json() - except RequestException as e: - if attempt < retries - 1: - click.echo(click.style(".", fg="yellow"), nl=False) - time.sleep(backoff_factor * (2**attempt)) - else: - raise APIRequestError( - click.style( - f"\nRequest to app_variant endpoint failed with status code {response.status_code} and error message: {e}.", - fg="red", - ) - ) - except Exception as e: - raise APIRequestError( - click.style(f"\nAn unexpected error occurred: {e}", fg="red") - ) - - -def start_variant( - variant_id: str, - host: str, - env_vars: Optional[Dict[str, str]] = None, - api_key: str = None, -) -> str: - """ - Starts or stops a container with the given variant and exposes its endpoint. - - Args: - variant_id (str): The ID of the variant. - host (str): The host URL. - env_vars (Optional[Dict[str, str]]): Optional environment variables to inject into the container. - api_key (str): The API key to use for the request. - - Returns: - str: The endpoint of the container. - - Raises: - APIRequestError: If the API request fails. - """ - payload = {} - payload["action"] = {"action": "START"} - if env_vars: - payload["env_vars"] = env_vars - try: - response = requests.put( - f"{host}/{BACKEND_URL_SUFFIX}/variants/{variant_id}/", - json=payload, - headers={"Authorization": api_key} if api_key is not None else None, - timeout=600, - ) - if response.status_code == 404: - raise APIRequestError( - f"404: Variant with ID {variant_id} does not exist on the server." - ) - elif response.status_code != 200: - error_message = response.text - raise APIRequestError( - f"Request to start variant endpoint failed with status code {response.status_code} and error message: {error_message}." - ) - return response.json().get("uri", "") - - except RequestException as e: - raise APIRequestError(f"An error occurred while making the request: {e}") - - -def list_variants(app_id: str, host: str, api_key: str = None) -> List[AppVariant]: - """ - Returns a list of AppVariant objects for a given app_id and host. - - Args: - app_id (str): The ID of the app to retrieve variants for. - host (str): The URL of the host to make the request to. - api_key (str): The API key to use for the request. - - Returns: - List[AppVariant]: A list of AppVariant objects for the given app_id and host. - """ - response = requests.get( - f"{host}/{BACKEND_URL_SUFFIX}/apps/{app_id}/variants/", - headers={"Authorization": api_key} if api_key is not None else None, - timeout=600, - ) - # Check for successful request - if response.status_code == 403: - raise APIRequestError( - f"No app by id {app_id} exists or you do not have access to it." - ) - elif response.status_code == 404: - raise APIRequestError( - f"No app by id {app_id} exists or you do not have access to it." - ) - elif response.status_code != 200: - error_message = response.json() - raise APIRequestError( - f"Request to apps endpoint failed with status code {response.status_code} and error message: {error_message}." - ) - - app_variants = response.json() - return [AppVariant(**variant) for variant in app_variants] - - -def remove_variant(variant_id: str, host: str, api_key: str = None): - """ - Sends a DELETE request to the Agenta backend to remove a variant with the given ID. - - Args: - variant_id (str): The ID of the variant to be removed. - host (str): The URL of the Agenta backend. - api_key (str): The API key to use for the request. - - Raises: - APIRequestError: If the request to the remove_variant endpoint fails. - - Returns: - None - """ - response = requests.delete( - f"{host}/{BACKEND_URL_SUFFIX}/variants/{variant_id}/", - headers={ - "Content-Type": "application/json", - "Authorization": api_key if api_key is not None else None, - }, - timeout=600, - ) - - # Check for successful request - if response.status_code != 200: - error_message = response.json() - raise APIRequestError( - f"Request to remove_variant endpoint failed with status code {response.status_code} and error message: {error_message}" - ) - - -def update_variant_image(variant_id: str, image: Image, host: str, api_key: str = None): - """ - Update the image of a variant with the given ID. - - Args: - variant_id (str): The ID of the variant to update. - image (Image): The new image to set for the variant. - host (str): The URL of the host to send the request to. - api_key (str): The API key to use for the request. - - Raises: - APIRequestError: If the request to update the variant fails. - - Returns: - None - """ - response = requests.put( - f"{host}/{BACKEND_URL_SUFFIX}/variants/{variant_id}/image/", - json=image.dict(), - headers={"Authorization": api_key} if api_key is not None else None, - timeout=600, - ) - if response.status_code != 200: - error_message = response.json() - raise APIRequestError( - f"Request to update app_variant failed with status code {response.status_code} and error message: {error_message}." - ) - - -def send_docker_tar( - app_id: str, base_name: str, tar_path: Path, host: str, api_key: str = None -) -> Image: - """ - Sends a Docker tar file to the specified host to build an image for the given app ID and variant name. - - Args: - app_id (str): The ID of the app. - base_name (str): The name of the codebase. - tar_path (Path): The path to the Docker tar file. - host (str): The URL of the host to send the request to. - api_key (str): The API key to use for the request. - - Returns: - Image: The built Docker image. - - Raises: - Exception: If the response status code is 500, indicating that serving the variant failed. - """ - with tar_path.open("rb") as tar_file: - response = requests.post( - f"{host}/{BACKEND_URL_SUFFIX}/containers/build_image/?app_id={app_id}&base_name={base_name}", - files={ - "tar_file": tar_file, - }, - headers={"Authorization": api_key} if api_key is not None else None, - timeout=1200, - ) - - if response.status_code == 500: - response_error = response.json() - error_msg = "Serving the variant failed.\n" - error_msg += f"Log: {response_error}\n" - error_msg += "Here's how you may be able to solve the issue:\n" - error_msg += "- First, make sure that the requirements.txt file has all the dependencies that you need.\n" - error_msg += "- Second, check the Docker logs for the backend image to see the error when running the Docker container." - raise Exception(error_msg) - - response.raise_for_status() - image = Image.parse_obj(response.json()) - return image - - -def save_variant_config( - base_id: str, - config_name: str, - parameters: Dict[str, Any], - overwrite: bool, - host: str, - api_key: Optional[str] = None, -) -> None: - """ - Saves a variant configuration to the Agenta backend. - If the config already exists, it will be overwritten if the overwrite argument is set to True. - If the config does does not exist, a new variant will be created. - - Args: - base_id (str): The ID of the base configuration. - config_name (str): The name of the variant configuration. - parameters (Dict[str, Any]): The parameters of the variant configuration. - overwrite (bool): Whether to overwrite an existing variant configuration with the same name. - host (str): The URL of the Agenta backend. - api_key (Optional[str], optional): The API key to use for authentication. Defaults to None. - - Raises: - ValueError: If the 'host' argument is not specified. - APIRequestError: If the request to the Agenta backend fails. - - Returns: - None - """ - if host is None: - raise ValueError("The 'host' is not specified in save_variant_config") - - variant_config = VariantConfigPayload( - base_id=base_id, - config_name=config_name, - parameters=parameters, - overwrite=overwrite, - ) - try: - response = requests.post( - f"{host}/{BACKEND_URL_SUFFIX}/configs/", - json=variant_config.dict(), - headers={"Authorization": api_key} if api_key is not None else None, - timeout=600, - ) - request = f"POST {host}/{BACKEND_URL_SUFFIX}/configs/ {variant_config.dict()}" - # Check for successful request - if response.status_code != 200: - error_message = response.json().get("detail", "Unknown error") - raise APIRequestError( - f"Request {request} to save_variant_config endpoint failed with status code {response.status_code}. Error message: {error_message}" - ) - except RequestException as e: - raise APIRequestError(f"Request failed: {str(e)}") - - -def fetch_variant_config( - base_id: str, - host: str, - config_name: Optional[str] = None, - environment_name: Optional[str] = None, - api_key: Optional[str] = None, -) -> Dict[str, Any]: - """ - Fetch a variant configuration from the server. - - Args: - base_id (str): ID of the base configuration. - config_name (str): Configuration name. - environment_name (str): Name of the environment. - host (str): The server host URL. - api_key (Optional[str], optional): The API key to use for authentication. Defaults to None. - - Raises: - APIRequestError: If the API request fails. - - Returns: - dict: The requested variant configuration. - """ - - if host is None: - raise ValueError("The 'host' is not specified in fetch_variant_config") - - try: - if environment_name: - endpoint_params = f"?base_id={base_id}&environment_name={environment_name}" - elif config_name: - endpoint_params = f"?base_id={base_id}&config_name={config_name}" - else: - raise ValueError( - "Either 'config_name' or 'environment_name' must be specified in fetch_variant_config" - ) - response = requests.get( - f"{host}/{BACKEND_URL_SUFFIX}/configs/{endpoint_params}", - headers={"Authorization": api_key} if api_key is not None else None, - timeout=600, - ) - - request = f"GET {host}/{BACKEND_URL_SUFFIX}/configs/ {base_id} {config_name} {environment_name}" - - # Check for successful request - if response.status_code != 200: - error_message = response.json().get("detail", "Unknown error") - raise APIRequestError( - f"Request {request} to fetch_variant_config endpoint failed with status code {response.status_code}. Error message: {error_message}" - ) - - return response.json() - - except RequestException as e: - raise APIRequestError(f"Request failed: {str(e)}") - - -def validate_api_key(api_key: str, host: str) -> bool: - """ - Validates an API key with the Agenta backend. - - Args: - api_key (str): The API key to validate. - host (str): The URL of the Agenta backend. - - Returns: - bool: Whether the API key is valid or not. - """ - try: - headers = {"Authorization": api_key} - - prefix = api_key.split(".")[0] - - response = requests.get( - f"{host}/{BACKEND_URL_SUFFIX}/keys/{prefix}/validate/", - headers=headers, - timeout=600, - ) - if response.status_code != 200: - error_message = response.json() - raise APIRequestError( - f"Request to validate api key failed with status code {response.status_code} and error message: {error_message}." - ) - return True - except RequestException as e: - raise APIRequestError(f"An error occurred while making the request: {e}") - - -def retrieve_user_id(host: str, api_key: Optional[str] = None) -> str: - """Retrieve user ID from the server. - - Args: - host (str): The URL of the Agenta backend - api_key (str): The API key to validate with. - - Returns: - str: the user ID - """ - - try: - response = requests.get( - f"{host}/{BACKEND_URL_SUFFIX}/profile/", - headers={"Authorization": api_key} if api_key is not None else None, - timeout=600, - ) - if response.status_code != 200: - error_message = response.json().get("detail", "Unknown error") - raise APIRequestError( - f"Request to fetch_user_profile endpoint failed with status code {response.status_code}. Error message: {error_message}" - ) - return response.json()["id"] - except RequestException as e: - raise APIRequestError(f"Request failed: {str(e)}") From bbc9a994297ef255cbf2e3f120e26e9529dd9806 Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 25 Dec 2023 15:29:16 +0100 Subject: [PATCH 139/267] format --- agenta-backend/agenta_backend/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/main.py b/agenta-backend/agenta_backend/main.py index 8b77910ce2..fc4df89b92 100644 --- a/agenta-backend/agenta_backend/main.py +++ b/agenta-backend/agenta_backend/main.py @@ -83,4 +83,5 @@ async def lifespan(application: FastAPI, cache=True): if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]: import agenta_backend.cloud.main as cloud - app = cloud.extend_app_schema(app) \ No newline at end of file + + app = cloud.extend_app_schema(app) From d7e6d67aab75f07f1e511ac7464c441ab2015dde Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 25 Dec 2023 15:41:50 +0100 Subject: [PATCH 140/267] set timeout to 600 for build_image function --- agenta-cli/agenta/client/backend/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-cli/agenta/client/backend/client.py b/agenta-cli/agenta/client/backend/client.py index 61bf7c852b..5cd775daa1 100644 --- a/agenta-cli/agenta/client/backend/client.py +++ b/agenta-cli/agenta/client/backend/client.py @@ -2104,7 +2104,7 @@ def build_image(self, *, app_id: str, base_name: str, tar_file: typing.IO) -> Im data=jsonable_encoder({}), files={"tar_file": tar_file}, headers=self._client_wrapper.get_headers(), - timeout=60, + timeout=600, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(Image, _response.json()) # type: ignore From 1aeec5329c3ce06aa9aa39d8f697e3f2ecb58c8f Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 25 Dec 2023 15:42:21 +0100 Subject: [PATCH 141/267] add step to set timeout --- agenta-cli/agenta/client/Readme.md | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/agenta-cli/agenta/client/Readme.md b/agenta-cli/agenta/client/Readme.md index dcd788efc8..36bb8993ab 100644 --- a/agenta-cli/agenta/client/Readme.md +++ b/agenta-cli/agenta/client/Readme.md @@ -59,15 +59,7 @@ fern init --openapi https://cloud.agenta.ai/api/openapi.json version: 0.6.0 ``` -6. Set timeout. - > Default timeout is 60 seconds but some operations in the CLI can take longer - Configure the python sdk to use a specified timeout by adding this configuration. - ```yaml - config: - timeout_in_seconds: 600 - ``` - -7. Change the path from this `path: ../generated/typescript` to this path: `../backend` +6. Change the path from this `path: ../generated/typescript` to this path: `../backend` Now your generators.yml should look like this; ```yaml @@ -80,16 +72,17 @@ fern init --openapi https://cloud.agenta.ai/api/openapi.json output: location: local-file-system path: ../backend - config: - timeout_in_seconds: 600 ``` -8. Go to the fern.config.json file and change the value of "organization" to `agenta` +7. Go to the fern.config.json file and change the value of "organization" to `agenta` -9. Generate the client code +8. Generate the client code ```bash fern generate ``` +9. Change the timeout for the build_image function endpoint + Go to the client.py in the generated code folder search for the `build_image` function in the AgentaApi class and change the timeout to 600 + 10. Delete the fern folder. \ No newline at end of file From 78ba8722037ce57c7a73c92fcc7b3a7596ffd87b Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 25 Dec 2023 15:52:37 +0100 Subject: [PATCH 142/267] Update Readme.md with image description --- agenta-cli/agenta/client/Readme.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/agenta-cli/agenta/client/Readme.md b/agenta-cli/agenta/client/Readme.md index 36bb8993ab..0850350b76 100644 --- a/agenta-cli/agenta/client/Readme.md +++ b/agenta-cli/agenta/client/Readme.md @@ -73,16 +73,24 @@ fern init --openapi https://cloud.agenta.ai/api/openapi.json location: local-file-system path: ../backend ``` + image + 7. Go to the fern.config.json file and change the value of "organization" to `agenta` + image + -8. Generate the client code +9. Generate the client code ```bash fern generate ``` -9. Change the timeout for the build_image function endpoint - Go to the client.py in the generated code folder search for the `build_image` function in the AgentaApi class and change the timeout to 600 +10. Change the timeout for the build_image function endpoint + Go to the client.py in the generated code folder search for the `build_image` function in the AgentaApi class and change the timeout to 600. + When done, it should look like this; + image + + -10. Delete the fern folder. \ No newline at end of file +11. Delete the fern folder. From a294424f1fe21c234c945e0993bef7056384325f Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 4 Jan 2024 12:21:43 +0100 Subject: [PATCH 143/267] Update main.py --- agenta-backend/agenta_backend/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/agenta-backend/agenta_backend/main.py b/agenta-backend/agenta_backend/main.py index fc4df89b92..832353525f 100644 --- a/agenta-backend/agenta_backend/main.py +++ b/agenta-backend/agenta_backend/main.py @@ -45,8 +45,7 @@ async def lifespan(application: FastAPI, cache=True): yield -# app = FastAPI(lifespan=lifespan) -app = FastAPI() +app = FastAPI(lifespan=lifespan) allow_headers = ["Content-Type"] From abf1ec16a276e4c4f51bd962a9560a80b8d47af8 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 4 Jan 2024 17:29:53 +0100 Subject: [PATCH 144/267] Fix result type conversion in evaluation tables --- .../ABTestingEvaluationTable.tsx | 4 ++++ .../AICritiqueEvaluationTable.tsx | 4 +++- .../CustomCodeRunEvaluationTable.tsx | 4 ++++ .../ExactMatchEvaluationTable.tsx | 4 ++++ .../EvaluationTable/RegexEvaluationTable.tsx | 4 ++++ .../SimilarityMatchEvaluationTable.tsx | 4 ++++ .../SingleModelEvaluationTable.tsx | 3 +++ .../EvaluationTable/WebhookEvaluationTable.tsx | 4 ++++ .../components/Playground/Views/TestView.tsx | 18 +++++++++++------- agenta-web/src/lib/services/api.ts | 6 ++++-- 10 files changed, 45 insertions(+), 10 deletions(-) diff --git a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx index 163e490af8..fb6331e2a8 100644 --- a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx @@ -21,6 +21,7 @@ import {testsetRowToChatMessages} from "@/lib/helpers/testset" import EvaluationVotePanel from "../Evaluations/EvaluationCardView/EvaluationVotePanel" import VariantAlphabet from "../Evaluations/EvaluationCardView/VariantAlphabet" import {ParamsFormWithRun} from "./SingleModelEvaluationTable" +import {PassThrough} from "stream" const {Title} = Typography @@ -238,6 +239,9 @@ const ABTestingEvaluationTable: React.FC = ({ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false) : [], ) + if (typeof result !== "string") { + result = result.message + } setRowValue(rowIndex, variant.variantId, result) ;(outputs as KeyValuePair)[variant.variantId] = result diff --git a/agenta-web/src/components/EvaluationTable/AICritiqueEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/AICritiqueEvaluationTable.tsx index 2dbc2aab3b..5fe2a4674e 100644 --- a/agenta-web/src/components/EvaluationTable/AICritiqueEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/AICritiqueEvaluationTable.tsx @@ -271,7 +271,9 @@ Answer ONLY with one of the given grading or evaluation options. ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false) : [], ) - + if (typeof result !== "string") { + result = result.message + } if (variantData[idx].isChatVariant) { result = contentToChatMessageString(result) } diff --git a/agenta-web/src/components/EvaluationTable/CustomCodeRunEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/CustomCodeRunEvaluationTable.tsx index 711321cf82..46a572f2d3 100644 --- a/agenta-web/src/components/EvaluationTable/CustomCodeRunEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/CustomCodeRunEvaluationTable.tsx @@ -249,6 +249,10 @@ const CustomCodeRunEvaluationTable: React.FC = ( ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false) : [], ) + if (typeof result !== "string") { + result = result.message + } + if (variantData[idx].isChatVariant) result = contentToChatMessageString(result) setRowValue(rowIndex, columnName as any, result) diff --git a/agenta-web/src/components/EvaluationTable/ExactMatchEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/ExactMatchEvaluationTable.tsx index 7f80ba22b4..43d00c8b08 100644 --- a/agenta-web/src/components/EvaluationTable/ExactMatchEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/ExactMatchEvaluationTable.tsx @@ -192,6 +192,10 @@ const ExactMatchEvaluationTable: React.FC = ({ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false) : [], ) + if (typeof result !== "string") { + result = result.message + } + if (variantData[idx].isChatVariant) result = contentToChatMessageString(result) setRowValue(rowIndex, columnName, result) diff --git a/agenta-web/src/components/EvaluationTable/RegexEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/RegexEvaluationTable.tsx index baa31e5dd5..8bec03d831 100644 --- a/agenta-web/src/components/EvaluationTable/RegexEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/RegexEvaluationTable.tsx @@ -220,6 +220,10 @@ const RegexEvaluationTable: React.FC = ({ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false) : [], ) + if (typeof result !== "string") { + result = result.message + } + if (variantData[idx].isChatVariant) result = contentToChatMessageString(result) const {regexPattern, regexShouldMatch} = form.getFieldsValue() diff --git a/agenta-web/src/components/EvaluationTable/SimilarityMatchEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/SimilarityMatchEvaluationTable.tsx index 1e294efb83..d19cf3ca63 100644 --- a/agenta-web/src/components/EvaluationTable/SimilarityMatchEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/SimilarityMatchEvaluationTable.tsx @@ -215,6 +215,10 @@ const SimilarityMatchEvaluationTable: React.FC = ({ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false) : [], ) + if (typeof result !== "string") { + result = result.message + } setRowValue(rowIndex, variant.variantId, result) ;(outputs as KeyValuePair)[variant.variantId] = result diff --git a/agenta-web/src/components/EvaluationTable/WebhookEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/WebhookEvaluationTable.tsx index cd3979f81c..c001c3834a 100644 --- a/agenta-web/src/components/EvaluationTable/WebhookEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/WebhookEvaluationTable.tsx @@ -199,6 +199,10 @@ const WebhookEvaluationTable: React.FC = ({ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false) : [], ) + if (typeof result !== "string") { + result = result.message + } + if (variantData[idx].isChatVariant) result = contentToChatMessageString(result) const {webhookUrl} = form.getFieldsValue() diff --git a/agenta-web/src/components/Playground/Views/TestView.tsx b/agenta-web/src/components/Playground/Views/TestView.tsx index 3509363815..d7ab8ee2ef 100644 --- a/agenta-web/src/components/Playground/Views/TestView.tsx +++ b/agenta-web/src/components/Playground/Views/TestView.tsx @@ -340,13 +340,17 @@ const App: React.FC = ({ variant.baseId || "", isChatVariant ? testItem.chat : [], ) - - setResultForIndex(res.message, index) - setAdditionalDataList((prev) => { - const newDataList = [...prev] - newDataList[index] = {cost: res.cost, latency: res.latency, usage: res.usage} - return newDataList - }) + // check if res is an object or string + if (typeof res === "string") { + setResultForIndex(res, index) + } else { + setResultForIndex(res.message, index) + setAdditionalDataList((prev) => { + const newDataList = [...prev] + newDataList[index] = {cost: res.cost, latency: res.latency, usage: res.usage} + return newDataList + }) + } } catch (e) { setResultForIndex( "The code has resulted in the following error: \n\n --------------------- \n" + diff --git a/agenta-web/src/lib/services/api.ts b/agenta-web/src/lib/services/api.ts index eb72630ba0..30eab1e56b 100644 --- a/agenta-web/src/lib/services/api.ts +++ b/agenta-web/src/lib/services/api.ts @@ -72,8 +72,10 @@ export function restartAppVariantContainer(variantId: string) { * @param inputParametersDict A dictionary of the input parameters to be passed to the variant endpoint * @param inputParamDefinition A list of the parameters that are defined in the openapi.json (these are only part of the input params, the rest is defined by the user in the optparms) * @param optionalParameters The optional parameters (prompt, models, AND DICTINPUTS WHICH ARE TO BE USED TO ADD INPUTS ) - * @param URIPath - * @returns + * @param appId - The ID of the app. + * @param baseId - The base ID. + * @param chatMessages - An optional array of chat messages. + * @returns A Promise that resolves with the response data from the POST request. */ export async function callVariant( inputParametersDict: KeyValuePair, From fdf797c34e9bf4f0bc7725c2756b01871f9e1aab Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Thu, 4 Jan 2024 18:29:27 +0100 Subject: [PATCH 145/267] remove cost, latency and usage if res is a string --- agenta-web/src/components/Playground/Views/TestView.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agenta-web/src/components/Playground/Views/TestView.tsx b/agenta-web/src/components/Playground/Views/TestView.tsx index d7ab8ee2ef..9da2f5fc20 100644 --- a/agenta-web/src/components/Playground/Views/TestView.tsx +++ b/agenta-web/src/components/Playground/Views/TestView.tsx @@ -166,7 +166,7 @@ const BoxComponent: React.FC = ({ imageSize="large" /> - {additionalData && ( + {additionalData.cost || additionalData.latency ? (

Tokens:{" "} @@ -187,6 +187,8 @@ const BoxComponent: React.FC = ({ : "0ms"}

+ ) : ( + "" )} From e8caf7e88e26746637198ea6d13ded0df83bb62f Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Thu, 4 Jan 2024 19:26:55 +0100 Subject: [PATCH 146/267] null checks added --- agenta-web/src/components/Playground/Views/TestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-web/src/components/Playground/Views/TestView.tsx b/agenta-web/src/components/Playground/Views/TestView.tsx index 9da2f5fc20..8f223ac68f 100644 --- a/agenta-web/src/components/Playground/Views/TestView.tsx +++ b/agenta-web/src/components/Playground/Views/TestView.tsx @@ -166,7 +166,7 @@ const BoxComponent: React.FC = ({ imageSize="large" /> - {additionalData.cost || additionalData.latency ? ( + {additionalData?.cost || additionalData?.latency ? (

Tokens:{" "} From 10f757939382732c2147d4f92fa081e9e9729f7b Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 8 Jan 2024 09:33:54 +0100 Subject: [PATCH 147/267] fix broken links --- docs/getting_started/introduction.mdx | 4 ++-- docs/mint.json | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/getting_started/introduction.mdx b/docs/getting_started/introduction.mdx index 285ef86737..f2e7a46246 100644 --- a/docs/getting_started/introduction.mdx +++ b/docs/getting_started/introduction.mdx @@ -11,8 +11,8 @@ Agenta is an open-source platform that helps **developers** and **product teams* 1. Rapidly [**experiment** and **compare** prompts](/basic_guides/prompt_engineering) on [any LLM workflow](/advanced_guides/custom_applications) (chain-of-prompts, Retrieval Augmented Generation (RAG), LLM agents...) 2. Rapidly [**create test sets**](/basic_guides/test_sets) and **golden datasets** for evaluation -3. [**Evaluate** your application](/basic_guides/automatic_evaluation) with pre-existing or [**custom evaluators**](/advanced_guides/using_custom_evaluators) -4. [**Annotate** and **A/B test**](/basic_guides/human_evaluation) your applications with **human feedback** +3. **Evaluate** your application with pre-existing or **custom evaluators** +4. **Annotate** and **A/B test** your applications with **human feedback** 5. [**Collaborate with product teams**](/basic_guides/team_management) for prompt engineering and evaluation 6. [**Deploy your application**](/basic_guides/deployment) in one-click in the UI, through CLI, or through github workflows. diff --git a/docs/mint.json b/docs/mint.json index 4d39930e84..8e1f137575 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -84,8 +84,7 @@ "pages": [ "basic_guides/creating_an_app", "basic_guides/prompt_engineering", - "basic_guides/test_sets", - "basic_guides/automatic_evaluation", + "basic_guides/test_sets", "basic_guides/deployment", "basic_guides/team_management" ] From 481d4310db4e7aae6a11c86da24b2d6d6f5d685b Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 8 Jan 2024 20:23:42 +0100 Subject: [PATCH 148/267] register_default now per default does not overwrite --- agenta-cli/agenta/sdk/agenta_init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/agenta-cli/agenta/sdk/agenta_init.py b/agenta-cli/agenta/sdk/agenta_init.py index 6803fecd80..df7ebb4709 100644 --- a/agenta-cli/agenta/sdk/agenta_init.py +++ b/agenta-cli/agenta/sdk/agenta_init.py @@ -1,3 +1,5 @@ +from agenta.client.exceptions import APIRequestError +from agenta.client.backend.client import AgentaApi import os import logging from typing import Any, Optional @@ -7,8 +9,6 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -from agenta.client.backend.client import AgentaApi -from agenta.client.exceptions import APIRequestError BACKEND_URL_SUFFIX = os.environ.get("BACKEND_URL_SUFFIX", "api") CLIENT_API_KEY = os.environ.get("AGENTA_API_KEY") @@ -104,11 +104,11 @@ def __init__(self, base_id, host): else: self.persist = True - def register_default(self, overwrite=True, **kwargs): + def register_default(self, overwrite=False, **kwargs): """alias for default""" return self.default(overwrite=overwrite, **kwargs) - def default(self, overwrite=True, **kwargs): + def default(self, overwrite=False, **kwargs): """Saves the default parameters to the app_name and base_name in case they are not already saved. Args: overwrite: Whether to overwrite the existing configuration or not From a5d3549c9c3c0d3937da4448465bcf02148a4682 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 8 Jan 2024 20:27:53 +0100 Subject: [PATCH 149/267] allow overwrite when parameters are empty even if overwrite is set to False --- agenta-backend/agenta_backend/routers/configs_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/routers/configs_router.py b/agenta-backend/agenta_backend/routers/configs_router.py index bb58060fcd..0f0704b762 100644 --- a/agenta-backend/agenta_backend/routers/configs_router.py +++ b/agenta-backend/agenta_backend/routers/configs_router.py @@ -44,7 +44,7 @@ async def save_config( variant_to_overwrite = variant_db break if variant_to_overwrite is not None: - if payload.overwrite: + if payload.overwrite or variant_to_overwrite.config.parameters == {}: print(f"update_variant_parameters ===> {payload.overwrite}") await app_manager.update_variant_parameters( app_variant_id=str(variant_to_overwrite.id), From af5b33f2cccf2e498ff868ba5ba0798a1918784d Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 8 Jan 2024 20:32:45 +0100 Subject: [PATCH 150/267] Update variant image now removes configuration --- agenta-backend/agenta_backend/services/app_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agenta-backend/agenta_backend/services/app_manager.py b/agenta-backend/agenta_backend/services/app_manager.py index b15c335c4f..aceb1a853e 100644 --- a/agenta-backend/agenta_backend/services/app_manager.py +++ b/agenta-backend/agenta_backend/services/app_manager.py @@ -141,6 +141,8 @@ async def update_variant_image( await db_manager.update_base(app_variant_db.base, image=db_image) # Update variant with new image app_variant_db = await db_manager.update_app_variant(app_variant_db, image=db_image) + # Update variant to remove configuration + await db_manager.update_variant_parameters(app_variant_db=app_variant_db, parameters={}) # Start variant await start_variant(app_variant_db, **kwargs) From a9c2d3518edb02839eb641965a736853beb268f0 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 8 Jan 2024 20:38:00 +0100 Subject: [PATCH 151/267] formatting --- agenta-backend/agenta_backend/services/app_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/services/app_manager.py b/agenta-backend/agenta_backend/services/app_manager.py index aceb1a853e..561c2d62c3 100644 --- a/agenta-backend/agenta_backend/services/app_manager.py +++ b/agenta-backend/agenta_backend/services/app_manager.py @@ -142,7 +142,9 @@ async def update_variant_image( # Update variant with new image app_variant_db = await db_manager.update_app_variant(app_variant_db, image=db_image) # Update variant to remove configuration - await db_manager.update_variant_parameters(app_variant_db=app_variant_db, parameters={}) + await db_manager.update_variant_parameters( + app_variant_db=app_variant_db, parameters={} + ) # Start variant await start_variant(app_variant_db, **kwargs) From 396eee4031d8e6c8a082f186c5431fbe4d3987a1 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 8 Jan 2024 20:50:45 +0100 Subject: [PATCH 152/267] Fix --- agenta-backend/agenta_backend/services/app_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/agenta-backend/agenta_backend/services/app_manager.py b/agenta-backend/agenta_backend/services/app_manager.py index 561c2d62c3..8b3258cbbd 100644 --- a/agenta-backend/agenta_backend/services/app_manager.py +++ b/agenta-backend/agenta_backend/services/app_manager.py @@ -139,12 +139,13 @@ async def update_variant_image( ) # Update base with new image await db_manager.update_base(app_variant_db.base, image=db_image) - # Update variant with new image - app_variant_db = await db_manager.update_app_variant(app_variant_db, image=db_image) # Update variant to remove configuration await db_manager.update_variant_parameters( app_variant_db=app_variant_db, parameters={} ) + # Update variant with new image + app_variant_db = await db_manager.update_app_variant(app_variant_db, image=db_image) + # Start variant await start_variant(app_variant_db, **kwargs) From 5b7ad6edfeafee04c25422cf1bf2880f5b088928 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 8 Jan 2024 21:11:18 +0100 Subject: [PATCH 153/267] Update link to LLM app tutorial --- .../src/components/AppSelector/modals/WriteOwnAppModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx b/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx index 0e9fa28692..a0242f6e8c 100644 --- a/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx +++ b/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx @@ -192,7 +192,7 @@ const WriteOwnAppModal: React.FC = ({...props}) => { Check out{" "} - + our tutorial for writing your first LLM app From 16d8c1b3440171f645538de1f0022da55dd573ed Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 8 Jan 2024 21:11:29 +0100 Subject: [PATCH 154/267] Update tutorial and template links --- docs/cookbook/list_templates.mdx | 6 +++--- docs/cookbook/list_templates_by_technology.mdx | 6 +++--- docs/cookbook/list_templates_by_use_case.mdx | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/cookbook/list_templates.mdx b/docs/cookbook/list_templates.mdx index 418fcbfd84..b86a6e9309 100644 --- a/docs/cookbook/list_templates.mdx +++ b/docs/cookbook/list_templates.mdx @@ -5,17 +5,17 @@ description: "A collection of templates and tutorials indexed by architecture." # Tutorials ## 📝 Text Generation -### [Single Prompt Application using OpenAI and Langchain](/tutorials/first-app-with-langchain) +### [Single Prompt Application using OpenAI and Langchain](/developer_guides/tutorials/first-app-with-langchain) Text Generation   OpenAI   Langchain   Learn how to use our SDK to deploy an application with agenta. The application we will create uses OpenAI and Langchain. The application generates outreach messages in Linkedin to investors based on a startup name and idea. -### [Use Mistral from Huggingface for a Summarization Task](/tutorials/deploy-mistral-model) +### [Use Mistral from Huggingface for a Summarization Task](/developer_guides/tutorials/deploy-mistral-model) Text Generation   Mistral   Hugging Face   Learn how to use a custom model with agenta. ## Retrieval Augmented Generation (RAG) -### [RAG Application with LlamaIndex](/tutorials/build-rag-application) +### [RAG Application with LlamaIndex](/developer_guides/tutorials/build-rag-application) Sales   OpenAI   RAG   LlamaIndex Learn how to create a RAG application with LlamaIndex and use it in agenta. You will create a playground in agenta where you can experiment with the parameters of the RAG application, test it and compare different versions. diff --git a/docs/cookbook/list_templates_by_technology.mdx b/docs/cookbook/list_templates_by_technology.mdx index bc7f38810f..1edeac6a53 100644 --- a/docs/cookbook/list_templates_by_technology.mdx +++ b/docs/cookbook/list_templates_by_technology.mdx @@ -6,14 +6,14 @@ description: "A collection of templates and tutorials indexed by the used framew This page is a work in progress. Please note that some of the entries are redundant. ## Langchain -### [Extraction using OpenAI Functions and Langchain](/templates/extract_job_information) +### [Extraction using OpenAI Functions and Langchain](/cookbook/extract_job_information) Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. ## LlamaIndex -### [RAG Application with LlamaIndex](/tutorials/build-rag-application) +### [RAG Application with LlamaIndex](/developer_guides/tutorials/build-rag-application) Learn how to create a RAG application with LlamaIndex and use it in agenta. You will create a playground in agenta where you can experiment with the parameters of the RAG application, test it and compare different versions. The application takes a sales transcript and answers questions based on it. ## OpenAI -### [Extraction using OpenAI Functions and Langchain](/templates/extract_job_information) +### [Extraction using OpenAI Functions and Langchain](/cookbook/extract_job_information) Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. diff --git a/docs/cookbook/list_templates_by_use_case.mdx b/docs/cookbook/list_templates_by_use_case.mdx index 9fa5954236..67f5c77f38 100644 --- a/docs/cookbook/list_templates_by_use_case.mdx +++ b/docs/cookbook/list_templates_by_use_case.mdx @@ -9,7 +9,7 @@ description: "A collection of templates and tutorials indexed by the the use cas Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. ## Sales -### [Single Prompt Application using OpenAI and Langchain](/tutorials/first-app-with-langchain) +### [Single Prompt Application using OpenAI and Langchain](/developer_guides/tutorials/first-app-with-langchain) Learn how to use our SDK to deploy an application with agenta. The application we will create uses OpenAI and Langchain. The application generates outreach messages in Linkedin to investors based on a startup name and idea. -### [RAG Application with LlamaIndex](/tutorials/build-rag-application) +### [RAG Application with LlamaIndex](/developer_guides/tutorials/build-rag-application) Learn how to create a RAG application with LlamaIndex and use it in agenta. You will create a playground in agenta where you can experiment with the parameters of the RAG application, test it and compare different versions. The application takes a sales transcript and answers questions based on it. From b23c990aede695b77caf03d477080f8020dc46dc Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 8 Jan 2024 21:11:37 +0100 Subject: [PATCH 155/267] Update links in deprecated documentation --- docs/depractated/learn/llm_app_architectures.mdx | 2 +- docs/depractated/learn/the_llmops_workflow.mdx | 2 +- docs/depractated/quickstart/getting-started-code.mdx | 4 ++-- docs/depractated/quickstart/how-agenta-works.mdx | 2 +- docs/depractated/quickstart/installation.mdx | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/depractated/learn/llm_app_architectures.mdx b/docs/depractated/learn/llm_app_architectures.mdx index 4ceb3adf92..75580a9f9b 100644 --- a/docs/depractated/learn/llm_app_architectures.mdx +++ b/docs/depractated/learn/llm_app_architectures.mdx @@ -10,7 +10,7 @@ There are multitude of architectures or pipelines for LLM applications. We discu This architecture is the simplest. The LLM application is a simple wrapper around one prompt / LLM call. -In agenta you can [create such LLM apps from the UI](/quickstart/getting-started-ui). Or you can [use your own code](quickstart/getting-started-code) in case that your [model is not supported](tutorials/deploy-mistral-model) (or you would like to add some custom logic for pre-processing or post-processing the inputs). +In agenta you can [create such LLM apps from the UI](/getting_started/getting-started-ui). Or you can [use your own code](quickstart/getting-started-code) in case that your [model is not supported](tutorials/deploy-mistral-model) (or you would like to add some custom logic for pre-processing or post-processing the inputs). diff --git a/docs/depractated/learn/the_llmops_workflow.mdx b/docs/depractated/learn/the_llmops_workflow.mdx index 397d7976b9..9012eea000 100644 --- a/docs/depractated/learn/the_llmops_workflow.mdx +++ b/docs/depractated/learn/the_llmops_workflow.mdx @@ -22,7 +22,7 @@ As a result, building AI applications is an **iterative process**. The LLMOps workflow is an iterative workflow with three main steps: experimentation, evaluation, and operation. The goal of the workflow is to iteratively improve the performance of the LLM application. The faster the iteration cycles and the number of experiments that can be run, the faster is the development process and the amount of use cases that the team can build. ### Experimentation -The workflow start usually by a proof of concept or an MVP of the application to be built. This require determining the [architecture to be used](/learn/llm_app_architectures) and either [writing the code for the first application](quickstart/getting-started-code) or starting from a pre-built [template](/quickstart/getting-started-ui). +The workflow start usually by a proof of concept or an MVP of the application to be built. This require determining the [architecture to be used](/learn/llm_app_architectures) and either [writing the code for the first application](quickstart/getting-started-code) or starting from a pre-built [template](/getting_started/getting-started-ui). After creating the first version, starts the [prompt engineering](/learn/prompt_engineering) part. The goal there is to find a set of prompts and parameters (temperature, model, etc.) that will give the best performance for the application. This is done by quickly experimenting with different prompts on a large set of inputs, visualizing the output, and understanding the effect of change. Another technique is to compare different configurations side-to-side to understand the effect of changes on the application. diff --git a/docs/depractated/quickstart/getting-started-code.mdx b/docs/depractated/quickstart/getting-started-code.mdx index 4b9042eb81..193b993e2a 100644 --- a/docs/depractated/quickstart/getting-started-code.mdx +++ b/docs/depractated/quickstart/getting-started-code.mdx @@ -4,7 +4,7 @@ description: 'Create and deploy your first LLM app in under 4 minutes' sidebarTitle: 'Creating LLM Apps using code' --- -This tutorial guides users through creating LLM apps via the command line interface. For simple applications that can be created from the UI, refer to [Getting Started](/quickstart/getting-started-code) +This tutorial guides users through creating LLM apps via the command line interface. For simple applications that can be created from the UI, refer to [Getting Started](/advanced_guides/custom_applications.mdx) Prefer video tutorial? Watch our 4-minute video [here](https://youtu.be/nggaRwDZM-0). @@ -15,7 +15,7 @@ Prefer video tutorial? Watch our 4-minute video [here](https://youtu.be/nggaRwDZ This guide outlines building a basic LLM app with **langchain** and **agenta**. By the end, you'll have a functional LLM app and knowledge of how to use the agenta CLI. -To learn more about creating an LLM app from scratch, please visit our [advanced tutorial](/tutorials/first-app-with-langchain). +To learn more about creating an LLM app from scratch, please visit our [advanced tutorial](/developer_guides/tutorials/first-app-with-langchain). ## Step 0: Installation diff --git a/docs/depractated/quickstart/how-agenta-works.mdx b/docs/depractated/quickstart/how-agenta-works.mdx index 6e94b8b8bc..c9b39232bc 100644 --- a/docs/depractated/quickstart/how-agenta-works.mdx +++ b/docs/depractated/quickstart/how-agenta-works.mdx @@ -44,7 +44,7 @@ Agenta's framework is based on three core concepts: - You can create an application using a pre-built template [directly from the UI](/quickstart/getting-started-code) or by writing [custom code](/quickstart/getting-started-code) and serving it using the CLI. + You can create an application using a pre-built template [directly from the UI](/advanced_guides/custom_applications.mdx) or by writing [custom code](/advanced_guides/custom_applications.mdx) and serving it using the CLI. Next, visit the playground to experiment with different configurations, prompts, and models. Directly observe the effects of changes or compare different variants side by side. diff --git a/docs/depractated/quickstart/installation.mdx b/docs/depractated/quickstart/installation.mdx index 4f23d5b401..3cc8db76f0 100644 --- a/docs/depractated/quickstart/installation.mdx +++ b/docs/depractated/quickstart/installation.mdx @@ -28,7 +28,7 @@ Use pip to install the SDK and CLI easily: pip install agenta ``` -Please see [contributing](/contributing/development-mode) for more information on how to install the SDK and CLI in developement mode. +Please see [contributing](/developer_guides/contributing/development-mode) for more information on how to install the SDK and CLI in developement mode. Agenta is under continuous developement, don't forget to always upgrade to the latest version of agenta using `pip install -U agenta`. @@ -57,6 +57,6 @@ Open your browser and go to [http://localhost](http://localhost). If you see the ## What's next? You're all set to start using Agenta! - + Click here to build your first LLM app in just 1 minute. \ No newline at end of file From 4549925433630f6a6fddab19ea4014eb365c9e2d Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 8 Jan 2024 21:11:59 +0100 Subject: [PATCH 156/267] Update links in documentation --- docs/developer_guides/cli/install.mdx | 4 ++-- docs/developer_guides/contributing/getting-started.mdx | 2 +- docs/getting_started/getting-started-ui.mdx | 4 ++-- docs/getting_started/introduction.mdx | 2 +- docs/mint.json | 8 ++++---- docs/self-host/host-locally.mdx | 3 +-- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/developer_guides/cli/install.mdx b/docs/developer_guides/cli/install.mdx index 5611bb8911..2e4ad384e9 100644 --- a/docs/developer_guides/cli/install.mdx +++ b/docs/developer_guides/cli/install.mdx @@ -14,10 +14,10 @@ pip install -U agenta # Quick usage guide - + Get an overview of the main commands and capabilities of agenta CLI - + Jump into a tutorial deploying an LLM app from code using agenta CLI diff --git a/docs/developer_guides/contributing/getting-started.mdx b/docs/developer_guides/contributing/getting-started.mdx index 4c4425f7be..1915744559 100644 --- a/docs/developer_guides/contributing/getting-started.mdx +++ b/docs/developer_guides/contributing/getting-started.mdx @@ -28,7 +28,7 @@ To maintain code quality, we adhere to certain formatting and linting rules: ## Contribution Steps -1. **Pick an Issue:** Start by selecting an issue from our issue tracker. Choose one that matches your skill set and begin coding. For more on this, read our [Creating an Issue Guide](/contributing/file-issue). +1. **Pick an Issue:** Start by selecting an issue from our issue tracker. Choose one that matches your skill set and begin coding. For more on this, read our [Creating an Issue Guide](/developer_guides/contributing/file-issue). 2. **Fork & Pull Request:** Fork our repository, create a new branch, add your changes, and submit a pull request. Ensure your code aligns with our standards and includes appropriate unit tests. diff --git a/docs/getting_started/getting-started-ui.mdx b/docs/getting_started/getting-started-ui.mdx index 9100a0e663..7d0f5dfd59 100644 --- a/docs/getting_started/getting-started-ui.mdx +++ b/docs/getting_started/getting-started-ui.mdx @@ -3,7 +3,7 @@ title: 'Quick Start' description: 'Create and deploy your first LLM app in one minute' --- -This tutorial helps users create LLM apps using templates within the UI. For more complex applications involving code in Agenta, please refer to Using code in Agenta [Using code in agenta](/quickstart/getting-started-code) +This tutorial helps users create LLM apps using templates within the UI. For more complex applications involving code in Agenta, please refer to Using code in Agenta [Using code in agenta](/advanced_guides/custom_applications) Want a video tutorial instead? We have a 4-minute video for you. [Watch it here](https://youtu.be/plPVrHXQ-DU). @@ -57,4 +57,4 @@ You can now find the API endpoint in the "Endpoints" menu. Copy and paste the co - Congratulations! You've created your first LLM application. Feel free to modify it, explore its parameters, and discover Agenta's features. Your next steps could include [building an application using your own code](/quickstart/getting-started-code.mdx), or following one of our UI-based tutorials. + Congratulations! You've created your first LLM application. Feel free to modify it, explore its parameters, and discover Agenta's features. Your next steps could include [building an application using your own code](/advanced_guides/custom_applications), or following one of our UI-based tutorials. diff --git a/docs/getting_started/introduction.mdx b/docs/getting_started/introduction.mdx index f2e7a46246..650ad4c104 100644 --- a/docs/getting_started/introduction.mdx +++ b/docs/getting_started/introduction.mdx @@ -39,7 +39,7 @@ By **adding a few lines to your application code**, you can create a prompt play Create and deploy your first app from the UI in under 2 minutes. diff --git a/docs/mint.json b/docs/mint.json index 8e1f137575..c9e70ff99f 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -84,7 +84,7 @@ "pages": [ "basic_guides/creating_an_app", "basic_guides/prompt_engineering", - "basic_guides/test_sets", + "basic_guides/test_sets", "basic_guides/deployment", "basic_guides/team_management" ] @@ -120,9 +120,9 @@ { "group": "Tutorials", "pages": [ - "developer_guides/tutorials/first-app-with-langchain", - "developer_guides/tutorials/build-rag-application", - "developer_guides/tutorials/deploy-mistral-model" + "developer_guides/developer_guides/tutorials/first-app-with-langchain", + "developer_guides/developer_guides/tutorials/build-rag-application", + "developer_guides/developer_guides/tutorials/deploy-mistral-model" ] }, { diff --git a/docs/self-host/host-locally.mdx b/docs/self-host/host-locally.mdx index 798c7848ec..208eedeb93 100644 --- a/docs/self-host/host-locally.mdx +++ b/docs/self-host/host-locally.mdx @@ -45,12 +45,11 @@ Open your browser and go to [http://localhost](http://localhost). If you see the If that is not the problem, please [file an issue in github](https://github.com/Agenta-AI/agenta/issues/new/choose) or reach out on [#support on Slack](https://join.slack.com/t/agenta-hq/shared_invite/zt-1zsafop5i-Y7~ZySbhRZvKVPV5DO_7IA). We are very active there and **should answer within minutes** (European time). - You can also check the most common problems in the [troubleshooting section](/howto/how-to-debug) of the documentation. ## What's next? You're all set to start using Agenta! - + Click here to build your first LLM app in just 1 minute. From 72f47e8729a9dacda92273b29f79cca6f1714b2a Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 8 Jan 2024 21:13:05 +0100 Subject: [PATCH 157/267] Update links in README.md --- README.md | 6 +- agenta-cli/README.md | 214 +++++++++++++++++++++++++++++-------------- 2 files changed, 149 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 87140fd14f..d667b815cc 100644 --- a/README.md +++ b/README.md @@ -101,15 +101,15 @@ Agenta allows developers and product teams to collaborate and build robust AI ap | Using an LLM App Template (For Non-Technical Users) | Starting from Code | | ------------- | ------------- | -|1. [Create an application using a pre-built template from our UI](https://cloud.agenta.ai?utm_source=github&utm_medium=readme&utm_campaign=github)
2. Access a playground where you can test and compare different prompts and configurations side-by-side.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |1. [Add a few lines to any LLM application code to automatically create a playground for it](https://docs.agenta.ai/tutorials/first-app-with-langchain)
2. Experiment with prompts and configurations, and compare them side-by-side in the playground.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. | +|1. [Create an application using a pre-built template from our UI](https://cloud.agenta.ai?utm_source=github&utm_medium=readme&utm_campaign=github)
2. Access a playground where you can test and compare different prompts and configurations side-by-side.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |1. [Add a few lines to any LLM application code to automatically create a playground for it](https://docs.agenta.ai/developer_guides/tutorials/first-app-with-langchain)
2. Experiment with prompts and configurations, and compare them side-by-side in the playground.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |

# Quick Start ### [Try the cloud version](https://cloud.agenta.ai?utm_source=github&utm_medium=readme&utm_campaign=github) -### [Create your first application in one-minute](https://docs.agenta.ai/quickstart/getting-started-ui) -### [Create an application using Langchain](https://docs.agenta.ai/tutorials/first-app-with-langchain) +### [Create your first application in one-minute](https://docs.agenta.ai/getting_started/getting-started-ui) +### [Create an application using Langchain](https://docs.agenta.ai/developer_guides/tutorials/first-app-with-langchain) ### [Self-host agenta](https://docs.agenta.ai/self-host/host-locally) ### [Read the Documentation](https://docs.agenta.ai) ### [Check the Cookbook](https://docs.agenta.ai/cookbook) diff --git a/agenta-cli/README.md b/agenta-cli/README.md index 4ed6b88633..d667b815cc 100644 --- a/agenta-cli/README.md +++ b/agenta-cli/README.md @@ -15,36 +15,46 @@

Quickly iterate, debug, and evaluate your LLM apps
- The open-source LLMOps platform for prompt-engineering, evaluation, and deployment of complex LLM apps. + The open-source LLMOps platform for prompt-engineering, evaluation, human feedback, and deployment of complex LLM apps.

MIT license. + + Doc + + PRs welcome Contributors Last Commit - Commits per month + Commits per month + + + PyPI - Downloads + +

-

- - - - - - - - - + + + + + + + + +

+ +
- + @@ -70,12 +80,11 @@

About • - DemoQuick StartInstallationFeaturesDocumentation • - Support • + EnterpriseCommunityContributing

@@ -84,54 +93,26 @@ # ℹ️ About -Building production-ready LLM-powered applications is currently very difficult. It involves countless iterations of prompt engineering, parameter tuning, and architectures. - -Agenta provides you with the tools to quickly do prompt engineering and 🧪 **experiment**, ⚖️ **evaluate**, and :rocket: **deploy** your LLM apps. All without imposing any restrictions on your choice of framework, library, or model. -

-
- - - - Overview agenta - -
+Agenta is an end-to-end LLMOps platform. It provides the tools for **prompt engineering and management**, ⚖️ **evaluation**, and :rocket: **deployment**. All without imposing any restrictions on your choice of framework, library, or model. +Agenta allows developers and product teams to collaborate and build robust AI applications in less time. -# Demo -https://github.com/Agenta-AI/agenta/assets/57623556/99733147-2b78-4b95-852f-67475e4ce9ed +## 🔨 How does it work? -# Quick Start +| Using an LLM App Template (For Non-Technical Users) | Starting from Code | +| ------------- | ------------- | +|1. [Create an application using a pre-built template from our UI](https://cloud.agenta.ai?utm_source=github&utm_medium=readme&utm_campaign=github)
2. Access a playground where you can test and compare different prompts and configurations side-by-side.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. |1. [Add a few lines to any LLM application code to automatically create a playground for it](https://docs.agenta.ai/developer_guides/tutorials/first-app-with-langchain)
2. Experiment with prompts and configurations, and compare them side-by-side in the playground.
3. Systematically evaluate your application using pre-built or custom evaluators.
4. Deploy the application to production with one click. | +

- +### [Try the cloud version](https://cloud.agenta.ai?utm_source=github&utm_medium=readme&utm_campaign=github) +### [Create your first application in one-minute](https://docs.agenta.ai/getting_started/getting-started-ui) +### [Create an application using Langchain](https://docs.agenta.ai/developer_guides/tutorials/first-app-with-langchain) +### [Self-host agenta](https://docs.agenta.ai/self-host/host-locally) +### [Read the Documentation](https://docs.agenta.ai) +### [Check the Cookbook](https://docs.agenta.ai/cookbook) # Features @@ -141,7 +122,7 @@ https://github.com/Agenta-AI/agenta/assets/57623556/99733147-2b78-4b95-852f-6747 https://github.com/Agenta-AI/agenta/assets/4510758/8b736d2b-7c61-414c-b534-d95efc69134c

Version Evaluation 📊

-Define test sets, the evaluate manually or programmatically your different variants.
+Define test sets, then evaluate manually or programmatically your different variants.
![](https://github.com/Agenta-AI/agenta/assets/4510758/b1de455d-7e0a-48d6-8497-39ba641600f0) @@ -154,9 +135,9 @@ When you are ready, deploy your LLM applications as APIs in one click.
## Why choose Agenta for building LLM-apps? - 🔨 **Build quickly**: You need to iterate many times on different architectures and prompts to bring apps to production. We streamline this process and allow you to do this in days instead of weeks. -- 🏗️ **Build robust apps and reduce hallucination**: We provide you with the tools to systematically and easily evaluate your application to make sure you only serve robust apps to production +- 🏗️ **Build robust apps and reduce hallucination**: We provide you with the tools to systematically and easily evaluate your application to make sure you only serve robust apps to production. - 👨‍💻 **Developer-centric**: We cater to complex LLM-apps and pipelines that require more than one simple prompt. We allow you to experiment and iterate on apps that have complex integration, business logic, and many prompts. -- 🌐 **Solution-Agnostic**: You have the freedom to use any library and models, be it Langchain, llma_index, or a custom-written alternative. +- 🌐 **Solution-Agnostic**: You have the freedom to use any libraries and models, be it Langchain, llma_index, or a custom-written alternative. - 🔒 **Privacy-First**: We respect your privacy and do not proxy your data through third-party services. The platform and the data are hosted on your infrastructure. ## How Agenta works: @@ -165,7 +146,7 @@ When you are ready, deploy your LLM applications as APIs in one click.
Write the code using any framework, library, or model you want. Add the `agenta.post` decorator and put the inputs and parameters in the function call just like in this example: -_Example simple application that generates baby names_ +_Example simple application that generates baby names:_ ```python import agenta as ag @@ -174,19 +155,19 @@ from langchain.llms import OpenAI from langchain.prompts import PromptTemplate default_prompt = "Give me five cool names for a baby from {country} with this gender {gender}!!!!" +ag.init() +ag.config(prompt_template=ag.TextParam(default_prompt), + temperature=ag.FloatParam(0.9)) - -@ag.post +@ag.entrypoint def generate( country: str, gender: str, - temperature: ag.FloatParam = 0.9, - prompt_template: ag.TextParam = default_prompt, ) -> str: - llm = OpenAI(temperature=temperature) + llm = OpenAI(temperature=ag.config.temperature) prompt = PromptTemplate( input_variables=["country", "gender"], - template=prompt_template, + template=ag.config.prompt_template, ) chain = LLMChain(llm=llm, prompt=prompt) output = chain.run(country=country, gender=gender) @@ -194,7 +175,7 @@ def generate( return output ``` -**2.Deploy your app using the Agenta CLI.** +**2.Deploy your app using the Agenta CLI** Screenshot 2023-06-19 at 15 58 34 @@ -205,3 +186,100 @@ Now your team can 🔄 iterate, 🧪 experiment, and ⚖️ evaluate different v Screenshot 2023-06-25 at 21 08 53 + +# Enterprise Support +Contact us here for enterprise support and early access to agenta self-managed enterprise with Kubernetes support.

+Book us + +# Disabling Anonymized Tracking + +To disable anonymized telemetry, set the following environment variable: + +- For web: Set `TELEMETRY_TRACKING_ENABLED` to `false` in your `agenta-web/.env` file. +- For CLI: Set `telemetry_tracking_enabled` to `false` in your `~/.agenta/config.toml` file. + +After making this change, restart agenta compose. + +# Contributing + +We warmly welcome contributions to Agenta. Feel free to submit issues, fork the repository, and send pull requests. + +We are usually hanging in our Slack. Feel free to [join our Slack and ask us anything](https://join.slack.com/t/agenta-hq/shared_invite/zt-1zsafop5i-Y7~ZySbhRZvKVPV5DO_7IA) + +Check out our [Contributing Guide](https://docs.agenta.ai/contributing/getting-started) for more information. + +## Contributors ✨ + + +[![All Contributors](https://img.shields.io/badge/all_contributors-39-orange.svg?style=flat-square)](#contributors-) + + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sameh Methnani
Sameh Methnani

💻 📖
Suad Suljovic
Suad Suljovic

💻 🎨 🧑‍🏫 👀
burtenshaw
burtenshaw

💻
Abram
Abram

💻 📖
Israel Abebe
Israel Abebe

🐛 🎨 💻
Master X
Master X

💻
corinthian
corinthian

💻 🎨
Pavle Janjusevic
Pavle Janjusevic

🚇
Kaosi Ezealigo
Kaosi Ezealigo

🐛 💻
Alberto Nunes
Alberto Nunes

🐛
Maaz Bin Khawar
Maaz Bin Khawar

💻 👀 🧑‍🏫
Nehemiah Onyekachukwu Emmanuel
Nehemiah Onyekachukwu Emmanuel

💻 💡 📖
Philip Okiokio
Philip Okiokio

📖
Abhinav Pandey
Abhinav Pandey

💻
Ramchandra Warang
Ramchandra Warang

💻 🐛
Biswarghya Biswas
Biswarghya Biswas

💻
Uddeepta Raaj Kashyap
Uddeepta Raaj Kashyap

💻
Nayeem Abdullah
Nayeem Abdullah

💻
Kang Suhyun
Kang Suhyun

💻
Yoon
Yoon

💻
Kirthi Bagrecha Jain
Kirthi Bagrecha Jain

💻
Navdeep
Navdeep

💻
Rhythm Sharma
Rhythm Sharma

💻
Osinachi Chukwujama
Osinachi Chukwujama

💻
莫尔索
莫尔索

📖
Agunbiade Adedeji
Agunbiade Adedeji

💻
Emmanuel Oloyede
Emmanuel Oloyede

💻 📖
Dhaneshwarguiyan
Dhaneshwarguiyan

💻
Priyanshu Prajapati
Priyanshu Prajapati

📖
Raviteja
Raviteja

💻
Arijit
Arijit

💻
Yachika9925
Yachika9925

📖
Aldrin
Aldrin

⚠️
seungduk.kim.2304
seungduk.kim.2304

💻
Andrei Dragomir
Andrei Dragomir

💻
diego
diego

💻
brockWith
brockWith

💻
Dennis Zelada
Dennis Zelada

💻
Romain Brucker
Romain Brucker

💻
+ + + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome! + +**Attribution**: Testing icons created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/testing) From 771b9f20e270eb45bc2e9e39897b62a88d99779c Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Tue, 9 Jan 2024 18:21:24 +0100 Subject: [PATCH 158/267] bug fix with alert when reloading page in playground and testset --- agenta-web/src/hooks/useBlockNavigation.ts | 24 ++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/agenta-web/src/hooks/useBlockNavigation.ts b/agenta-web/src/hooks/useBlockNavigation.ts index 47a2b355b9..8d73eb5a47 100644 --- a/agenta-web/src/hooks/useBlockNavigation.ts +++ b/agenta-web/src/hooks/useBlockNavigation.ts @@ -13,8 +13,27 @@ const useBlockNavigation = ( const props = useRef(_props) const shouldAlert = useRef(_shouldAlert) + const beforeUnloadHandler = (event: BeforeUnloadEvent) => { + if (blocking.current) { + const message = "You have unsaved changes. Are you sure you want to leave?" + event.returnValue = message // Standard for most browsers + return message // For some older browsers + } + } + useEffect(() => { blocking.current = _blocking + + // prevent from reload or closing tab with unsaved changes + if (blocking.current) { + window.addEventListener("beforeunload", beforeUnloadHandler) + } else { + window.removeEventListener("beforeunload", beforeUnloadHandler) + } + + return () => { + window.removeEventListener("beforeunload", beforeUnloadHandler) + } }, [_blocking]) useEffect(() => { @@ -26,9 +45,6 @@ const useBlockNavigation = ( }, [_shouldAlert]) useEffect(() => { - // prevent from reload or closing tab - window.onbeforeunload = () => true - const handler = (newRoute: string) => { if (opened.current || !blocking.current) return @@ -76,7 +92,7 @@ const useBlockNavigation = ( Router.events.on("routeChangeStart", handler) return () => { - window.onbeforeunload = null + window.removeEventListener("beforeunload", beforeUnloadHandler) Router.events.off("routeChangeStart", handler) } }, []) From 034925ce722571fd89a1642cba829194a28cae45 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Tue, 9 Jan 2024 18:27:04 +0100 Subject: [PATCH 159/267] cleanup --- agenta-web/src/hooks/useBlockNavigation.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/agenta-web/src/hooks/useBlockNavigation.ts b/agenta-web/src/hooks/useBlockNavigation.ts index 8d73eb5a47..be75439922 100644 --- a/agenta-web/src/hooks/useBlockNavigation.ts +++ b/agenta-web/src/hooks/useBlockNavigation.ts @@ -25,11 +25,7 @@ const useBlockNavigation = ( blocking.current = _blocking // prevent from reload or closing tab with unsaved changes - if (blocking.current) { - window.addEventListener("beforeunload", beforeUnloadHandler) - } else { - window.removeEventListener("beforeunload", beforeUnloadHandler) - } + window.addEventListener("beforeunload", beforeUnloadHandler) return () => { window.removeEventListener("beforeunload", beforeUnloadHandler) From 4d09aed4cc3d514ef6533776c6e0c210138272da Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 10 Jan 2024 00:02:38 +0100 Subject: [PATCH 160/267] Feat - created migrations module directory --- agenta-backend/agenta_backend/migrations/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 agenta-backend/agenta_backend/migrations/__init__.py diff --git a/agenta-backend/agenta_backend/migrations/__init__.py b/agenta-backend/agenta_backend/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 6d1ba47a140e37bc6a61b9ef953726d7b8f04f69 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 10 Jan 2024 00:36:26 +0100 Subject: [PATCH 161/267] Cleanup - remove duplicate field deletable in ImageDB document --- agenta-backend/agenta_backend/models/db_models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/agenta-backend/agenta_backend/models/db_models.py b/agenta-backend/agenta_backend/models/db_models.py index af8ab847b1..c336badf19 100644 --- a/agenta-backend/agenta_backend/models/db_models.py +++ b/agenta-backend/agenta_backend/models/db_models.py @@ -1,6 +1,6 @@ from uuid import uuid4 from datetime import datetime -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field from beanie import Document, Link, PydanticObjectId @@ -65,7 +65,6 @@ class ImageDB(Document): organization: Link[OrganizationDB] created_at: Optional[datetime] = Field(default=datetime.utcnow()) updated_at: Optional[datetime] = Field(default=datetime.utcnow()) - deletable: bool = Field(default=True) class Settings: name = "docker_images" From 6489daad12c7de5b8ec906fd8d6257f644b61527 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 10 Jan 2024 11:58:24 +0100 Subject: [PATCH 162/267] Update - added old evaluation db models --- .../agenta_backend/models/db_models.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/agenta-backend/agenta_backend/models/db_models.py b/agenta-backend/agenta_backend/models/db_models.py index c336badf19..35e0982f1f 100644 --- a/agenta-backend/agenta_backend/models/db_models.py +++ b/agenta-backend/agenta_backend/models/db_models.py @@ -360,3 +360,70 @@ class TraceDB(Document): class Settings: name = "traces" + + +class OldEvaluationTypeSettings(BaseModel): + similarity_threshold: Optional[float] + regex_pattern: Optional[str] + regex_should_match: Optional[bool] + webhook_url: Optional[str] + llm_app_prompt_template: Optional[str] + custom_code_evaluation_id: Optional[str] + evaluation_prompt_template: Optional[str] + + +class OldEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class OldEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class OldEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + evaluation_type_settings: OldEvaluationTypeSettings + variants: List[PydanticObjectId] + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluations" + + +class OldEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[EvaluationDB] + inputs: List[OldEvaluationScenarioInput] + outputs: List[OldEvaluationScenarioOutput] # EvaluationScenarioOutput + vote: Optional[str] + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "evaluation_scenarios" + + +class OldCustomEvaluationDB(Document): + evaluation_name: str + python_code: str + app: Link[AppDB] + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "custom_evaluations" From 55aa3fe068db80694b42b7f6d3a85046a980b749 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 10 Jan 2024 12:03:04 +0100 Subject: [PATCH 163/267] Update - added old evaluation db models to beanie --- agenta-backend/agenta_backend/models/db_engine.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/models/db_engine.py b/agenta-backend/agenta_backend/models/db_engine.py index b951be23a0..0c79e902f7 100644 --- a/agenta-backend/agenta_backend/models/db_engine.py +++ b/agenta-backend/agenta_backend/models/db_engine.py @@ -24,6 +24,8 @@ HumanEvaluationScenarioDB, EvaluationDB, EvaluationScenarioDB, + OldEvaluationDB, + OldEvaluationScenarioDB, SpanDB, TraceDB, ) @@ -51,6 +53,8 @@ HumanEvaluationScenarioDB, EvaluationDB, EvaluationScenarioDB, + OldEvaluationDB, + OldEvaluationScenarioDB, SpanDB, TraceDB, ] @@ -75,7 +79,6 @@ async def init_db(self): client = await self.initialize_client() db_name = self._get_database_name(self.mode) - await init_beanie(database=client[db_name], document_models=document_models) logger.info(f"Using {db_name} database...") From 515db5eb7a8cb9e63a644b47e9fba14944cdff67 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 10 Jan 2024 12:03:25 +0100 Subject: [PATCH 164/267] Feat - created migration file --- .../20240110001454_initial_migration.py | 123 ++++++++++++++++++ .../agenta_backend/migrations/__init__.py | 0 2 files changed, 123 insertions(+) create mode 100644 agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py delete mode 100644 agenta-backend/agenta_backend/migrations/__init__.py diff --git a/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py b/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py new file mode 100644 index 0000000000..e0e7e81353 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py @@ -0,0 +1,123 @@ +from agenta_backend.models.db_models import ( + AppDB, + OrganizationDB, + UserDB, + TestSetDB, + EvaluationDB, + EvaluationScenarioDB, + EvaluationScenarioInputDB, + EvaluationScenarioOutputDB, + EvaluatorConfigDB, + HumanEvaluationDB, + HumanEvaluationScenarioDB, + HumanEvaluationScenarioInput, + HumanEvaluationScenarioOutput, + OldEvaluationDB, + OldEvaluationScenarioDB, +) + +from beanie import free_fall_migration, Link + + +class Forward: + @free_fall_migration(document_models=[OldEvaluationDB, EvaluationDB]) + async def move_old_evals_to_new_evals_document(self, session): + async for old_eval in OldEvaluationDB.find_all(): + eval_config = EvaluatorConfigDB( + app=Link(AppDB, old_eval.app.id), + organization=Link(OrganizationDB, old_eval.organization.id), + user=Link(UserDB, old_eval.user.id), + evaluator_key=old_eval.evaluation_type, + settings_values={}, + ) + await eval_config.create() + if old_eval.evaluation_type in ["human_a_b_testing", "single_model_test"]: + new_eval = HumanEvaluationDB( + app=Link(AppDB, old_eval.app.id), + organization=Link(OrganizationDB, old_eval.organization.id), + user=Link(UserDB, old_eval.user.id), + status=old_eval.status, + evaluation_type=old_eval.evaluation_type, + variants=old_eval.variants, + testset=Link(TestSetDB, old_eval.testset.id), + ) + else: + new_eval = EvaluationDB( + app=Link(AppDB, old_eval.app.id), + organization=Link(OrganizationDB, old_eval.organization.id), + user=Link(UserDB, old_eval.user.id), + status=old_eval.status, + testset=Link(TestSetDB, old_eval.testset.id), + variant=old_eval.variants[0], + evaluator_configs=eval_config.id, + aggregated_results=[], + ) + await old_eval.delete() + await new_eval.replace(session=session) + + @free_fall_migration( + document_models=[OldEvaluationScenarioDB, EvaluationScenarioDB] + ) + async def move_old_eval_scenarios_to_new_eval_scenarios(self, session): + async for old_scenario in OldEvaluationScenarioDB.find_all(): + if old_scenario.evaluation_type in [ + "human_a_b_testing", + "single_model_test", + ]: + new_scenario = HumanEvaluationScenarioDB( + user=Link(UserDB, old_scenario.user.id), + organization=Link(OrganizationDB, old_scenario.organization.id), + evaluation=Link(EvaluationDB, old_scenario.evaluation.id), + inputs=[ + HumanEvaluationScenarioInput( + name=input.input_name, + value=input.input_value, + ) + for input in old_scenario.inputs + ], + outputs=[ + HumanEvaluationScenarioOutput( + variant_id=output.variant_id, + variant_output=output.variant_output, + ) + for output in old_scenario.outputs + ], + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + vote=old_scenario.vote, + score=old_scenario.score, + ) + else: + new_scenario = EvaluationScenarioDB( + user=Link(UserDB, old_scenario.user.id), + organization=Link(OrganizationDB, old_scenario.organization.id), + evaluation=Link(EvaluationDB, old_scenario.evaluation.id), + variant_id=old_scenario.evaluation.variants[0], + inputs=[ + EvaluationScenarioInputDB( + name=input.input_name, + type=type(input.input_value).__name__, + value=input.input_value, + ) + for input in old_scenario.inputs + ], + outputs=[ + EvaluationScenarioOutputDB( + type=type(output.variant_output).__name__, + value=output.variant_output, + ) + for output in old_scenario.outputs + ], + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + evaluators_configs=old_scenario.evaluation.evaluators_configs, + results=[], + ) + await old_scenario.delete() + await new_scenario.replace(session=session) + + +class Backward: + ... diff --git a/agenta-backend/agenta_backend/migrations/__init__.py b/agenta-backend/agenta_backend/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From b4b35df0de948dd7bdd6c2a25cd134792109fce1 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 10 Jan 2024 15:52:06 +0100 Subject: [PATCH 165/267] Update - modified migration script --- .../20240110001454_initial_migration.py | 264 ++++++++++++++++-- 1 file changed, 245 insertions(+), 19 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py b/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py index e0e7e81353..287c789190 100644 --- a/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py +++ b/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py @@ -1,32 +1,252 @@ -from agenta_backend.models.db_models import ( - AppDB, - OrganizationDB, - UserDB, - TestSetDB, - EvaluationDB, - EvaluationScenarioDB, - EvaluationScenarioInputDB, - EvaluationScenarioOutputDB, - EvaluatorConfigDB, - HumanEvaluationDB, - HumanEvaluationScenarioDB, - HumanEvaluationScenarioInput, - HumanEvaluationScenarioOutput, - OldEvaluationDB, - OldEvaluationScenarioDB, -) +from datetime import datetime +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field from beanie import free_fall_migration, Link +from beanie import Document, Link, PydanticObjectId + + +class OrganizationDB(Document): + name: str = Field(default="agenta") + description: str = Field(default="") + type: Optional[str] + owner: str # user id + members: Optional[List[PydanticObjectId]] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "organizations" + + +class UserDB(Document): + uid: str = Field(default="0", unique=True, index=True) + username: str = Field(default="agenta") + email: str = Field(default="demo@agenta.ai", unique=True) + organizations: Optional[List[PydanticObjectId]] = [] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "users" + + +class AppDB(Document): + app_name: str + organization: Link[OrganizationDB] + user: Link[UserDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "app_db" + + +class TestSetDB(Document): + name: str + app: Link[AppDB] + csvdata: List[Dict[str, str]] + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "testsets" + + +class EvaluatorConfigDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + name: str + evaluator_key: str + settings_values: Optional[Dict[str, Any]] = None + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluators_configs" + + +class Result(BaseModel): + type: str + value: Any + + +class EvaluationScenarioResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class AggregatedResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class EvaluationScenarioInputDB(BaseModel): + name: str + type: str + value: str + + +class EvaluationScenarioOutputDB(BaseModel): + type: str + value: Any + + +class HumanEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class HumanEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class HumanEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + variants: List[PydanticObjectId] + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "human_evaluations" + + +class HumanEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[HumanEvaluationDB] + inputs: List[HumanEvaluationScenarioInput] + outputs: List[HumanEvaluationScenarioOutput] + vote: Optional[str] + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "human_evaluations_scenarios" + + +class EvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str = Field(default="EVALUATION_INITIALIZED") + testset: Link[TestSetDB] + variant: PydanticObjectId + evaluators_configs: List[PydanticObjectId] + aggregated_results: List[AggregatedResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluations" + + +class EvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[EvaluationDB] + variant_id: PydanticObjectId + inputs: List[EvaluationScenarioInputDB] + outputs: List[EvaluationScenarioOutputDB] + correct_answer: Optional[str] + is_pinned: Optional[bool] + note: Optional[str] + evaluators_configs: List[PydanticObjectId] + results: List[EvaluationScenarioResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluation_scenarios" + + +class OldEvaluationTypeSettings(BaseModel): + similarity_threshold: Optional[float] + regex_pattern: Optional[str] + regex_should_match: Optional[bool] + webhook_url: Optional[str] + llm_app_prompt_template: Optional[str] + custom_code_evaluation_id: Optional[str] + evaluation_prompt_template: Optional[str] + + +class OldEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class OldEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class OldEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + evaluation_type_settings: OldEvaluationTypeSettings + variants: List[PydanticObjectId] + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluations" + + +class OldEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[EvaluationDB] + inputs: List[OldEvaluationScenarioInput] + outputs: List[OldEvaluationScenarioOutput] # EvaluationScenarioOutput + vote: Optional[str] + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "evaluation_scenarios" + class Forward: - @free_fall_migration(document_models=[OldEvaluationDB, EvaluationDB]) + @free_fall_migration( + document_models=[ + AppDB, + OrganizationDB, + UserDB, + EvaluatorConfigDB, + OldEvaluationDB, + EvaluationDB, + ] + ) async def move_old_evals_to_new_evals_document(self, session): async for old_eval in OldEvaluationDB.find_all(): eval_config = EvaluatorConfigDB( app=Link(AppDB, old_eval.app.id), organization=Link(OrganizationDB, old_eval.organization.id), user=Link(UserDB, old_eval.user.id), + name=f"{old_eval.app.app_name}_{old_eval.evaluation_type}", evaluator_key=old_eval.evaluation_type, settings_values={}, ) @@ -56,7 +276,13 @@ async def move_old_evals_to_new_evals_document(self, session): await new_eval.replace(session=session) @free_fall_migration( - document_models=[OldEvaluationScenarioDB, EvaluationScenarioDB] + document_models=[ + AppDB, + OrganizationDB, + UserDB, + OldEvaluationScenarioDB, + EvaluationScenarioDB, + ] ) async def move_old_eval_scenarios_to_new_eval_scenarios(self, session): async for old_scenario in OldEvaluationScenarioDB.find_all(): From 0e3957ea41020879b7754391d642c71f72b14899 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Wed, 10 Jan 2024 16:20:40 +0100 Subject: [PATCH 166/267] Update pyproject.toml --- agenta-cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-cli/pyproject.toml b/agenta-cli/pyproject.toml index 6a57d02fa6..36618f6acb 100644 --- a/agenta-cli/pyproject.toml +++ b/agenta-cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "agenta" -version = "0.6.10" +version = "0.7.0" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" authors = ["Mahmoud Mabrouk "] From 9e15233031878a6dc4f28c0dcb533ff838d2c787 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Wed, 10 Jan 2024 16:24:56 +0100 Subject: [PATCH 167/267] feat to cancel ongoing request --- .../components/Playground/Views/TestView.tsx | 148 +++++++++++++++--- agenta-web/src/lib/services/api.ts | 38 ++++- 2 files changed, 153 insertions(+), 33 deletions(-) diff --git a/agenta-web/src/components/Playground/Views/TestView.tsx b/agenta-web/src/components/Playground/Views/TestView.tsx index 31e7fc4b77..234eb60e0f 100644 --- a/agenta-web/src/components/Playground/Views/TestView.tsx +++ b/agenta-web/src/components/Playground/Views/TestView.tsx @@ -1,6 +1,6 @@ -import React, {useContext, useEffect, useState} from "react" +import React, {useContext, useEffect, useRef, useState} from "react" import {Button, Input, Card, Row, Col, Space, Form} from "antd" -import {CaretRightOutlined, PlusOutlined} from "@ant-design/icons" +import {CaretRightOutlined, CloseCircleOutlined, PlusOutlined} from "@ant-design/icons" import {callVariant} from "@/lib/services/api" import {ChatMessage, ChatRole, GenericObject, Parameter, Variant} from "@/lib/Types" import {batchExecute, randString, removeKeys} from "@/lib/helpers/utils" @@ -109,6 +109,7 @@ interface BoxComponentProps { onDelete?: () => void isChatVariant?: boolean variant: Variant + onCancel: () => void } const BoxComponent: React.FC = ({ @@ -122,6 +123,7 @@ const BoxComponent: React.FC = ({ onDelete, isChatVariant = false, variant, + onCancel, }) => { const {appTheme} = useAppTheme() const classes = useStylesBox() @@ -208,17 +210,23 @@ const BoxComponent: React.FC = ({ disabled={loading || !result} shape="round" /> - + {loading ? ( + + ) : ( + + )}
{!isChatVariant && ( @@ -276,6 +284,14 @@ const App: React.FC = ({ }> >(testList.map(() => ({cost: null, latency: null, usage: null}))) + const abortControllersRef = useRef([]) + + useEffect(() => { + return () => { + abortControllersRef.current.forEach((controller) => controller.abort()) + } + }, []) + useEffect(() => { setResultsList((prevResultsList) => { const newResultsList = testList.map((_, index) => { @@ -326,6 +342,14 @@ const App: React.FC = ({ } const handleRun = async (index: number) => { + // Cancel the existing request if it's still running + if (abortControllersRef.current[index]) { + abortControllersRef.current[index].abort() + } + + // Create a new AbortController for the current request + const controller = new AbortController() + abortControllersRef.current[index] = controller try { const testItem = testList[index] if (compareMode && !isRunning[index]) { @@ -355,20 +379,32 @@ const App: React.FC = ({ appId || "", variant.baseId || "", isChatVariant ? testItem.chat : [], + controller.signal, // Pass the signal to allow manual cancellation ) - // check if res is an object or string - if (typeof res === "string") { - setResultForIndex(res, index) - } else { - setResultForIndex(res.message, index) - setAdditionalDataList((prev) => { - const newDataList = [...prev] - newDataList[index] = {cost: res.cost, latency: res.latency, usage: res.usage} - return newDataList - }) + + // Check if the request was not cancelled + if (!controller.signal.aborted) { + // Update the result and additional data + if (typeof res === "string") { + setResultForIndex(res, index) + } else { + setResultForIndex(res.message, index) + setAdditionalDataList((prev) => { + const newDataList = [...prev] + newDataList[index] = { + cost: res.cost, + latency: res.latency, + usage: res.usage, + } + return newDataList + }) + } } } catch (e) { - setResultForIndex(`❌ ${getErrorMessage(e)}`, index) + // Check if the error is not due to cancellation + if (!controller.signal.aborted) { + setResultForIndex(`❌ ${getErrorMessage(e)}`, index) + } } finally { setIsRunning((prevState) => { const newState = [...prevState] @@ -378,11 +414,44 @@ const App: React.FC = ({ } } + const handleCancelAll = () => { + abortControllersRef.current.forEach((controller, index) => { + if (controller) { + controller.abort() + // Update the loading state to false + setIsRunning((prevState) => { + const newState = [...prevState] + newState[index] = false + return newState + }) + + // Optional: If you want to clear the result or perform additional cleanup + setResultForIndex("", index) + setAdditionalDataList((prev) => { + const newDataList = [...prev] + newDataList[index] = {cost: null, latency: null, usage: null} + return newDataList + }) + } + }) + } + + // ... + const handleRunAll = () => { + // Clear any existing abort controllers + abortControllersRef.current.forEach((controller) => controller && controller.abort()) + + // Create new abort controllers for each request + const newAbortControllers = Array(testList.length) + .fill(undefined) + .map(() => new AbortController()) + abortControllersRef.current = newAbortControllers + const funcs: Function[] = [] rootRef.current ?.querySelectorAll("[data-cy=testview-input-parameters-run-button]") - .forEach((btn) => funcs.push(() => (btn as HTMLButtonElement).click())) + .forEach((btn, index) => funcs.push(() => handleRun(index))) batchExecute(funcs) } @@ -425,6 +494,27 @@ const App: React.FC = ({ } } + const handleCancel = (index: number) => { + if (abortControllersRef.current[index]) { + abortControllersRef.current[index].abort() + } + + // Update the loading state to false + setIsRunning((prevState) => { + const newState = [...prevState] + newState[index] = false + return newState + }) + + // Optional: If you want to clear the result or perform additional cleanup + setResultForIndex("", index) + setAdditionalDataList((prev) => { + const newDataList = [...prev] + newDataList[index] = {cost: null, latency: null, usage: null} + return newDataList + }) + } + return (
@@ -440,6 +530,13 @@ const App: React.FC = ({ > Run all +
@@ -463,6 +560,7 @@ const App: React.FC = ({ onDelete={testList.length >= 2 ? () => handleDeleteRow(index) : undefined} isChatVariant={isChatVariant} variant={variant} + onCancel={() => handleCancel(index)} /> ))} - diff --git a/agenta-web/src/lib/services/api.ts b/agenta-web/src/lib/services/api.ts index 1aaccf1b75..cd5a438605 100644 --- a/agenta-web/src/lib/services/api.ts +++ b/agenta-web/src/lib/services/api.ts @@ -1,6 +1,5 @@ import useSWR from "swr" import axios from "@/lib//helpers/axiosConfig" -import ax, {CancelToken} from "axios" import { detectChatVariantFromOpenAISchema, openAISchemaToParameters, @@ -85,7 +84,7 @@ export async function callVariant( appId: string, baseId: string, chatMessages?: ChatMessage[], - signal?: AbortSignal, // New parameter for the AbortController signal + signal?: AbortSignal, ) { const isChatVariant = Array.isArray(chatMessages) && chatMessages.length > 0 // Separate input parameters into two dictionaries based on the 'input' property @@ -121,30 +120,9 @@ export async function callVariant( const appContainerURI = await getAppContainerURL(appId, undefined, baseId) - // Pass the signal to the axios request using CancelToken.source() - const {token, cancel} = CancelToken.source() - if (signal) { - signal.addEventListener("abort", () => { - // Cancel the axios request when the AbortController is aborted - cancel() - }) - } - - return ax - .post(`${appContainerURI}/generate`, requestBody, { - cancelToken: token, // Pass the CancelToken to link with the AbortController signal - }) - .then((res) => { - return res.data - }) - .catch((error) => { - if (ax.isCancel(error)) { - throw new Error("Request canceled") - } else { - // Handle other errors - throw error - } - }) + return axios.post(`${appContainerURI}/generate`, requestBody, {signal}).then((res) => { + return res.data + }) } /** From 59d09f52bdf6c57e5346a0889ca26cffc1d26f4d Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Thu, 11 Jan 2024 12:57:51 +0100 Subject: [PATCH 172/267] revert changes and improved ui --- .../components/Playground/Views/TestView.tsx | 34 +++++++++---------- agenta-web/src/lib/services/api.ts | 8 +++-- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/agenta-web/src/components/Playground/Views/TestView.tsx b/agenta-web/src/components/Playground/Views/TestView.tsx index 0c0a342f5b..b0317321f7 100644 --- a/agenta-web/src/components/Playground/Views/TestView.tsx +++ b/agenta-web/src/components/Playground/Views/TestView.tsx @@ -211,7 +211,12 @@ const BoxComponent: React.FC = ({ shape="round" /> {loading ? ( - ) : ( @@ -380,21 +385,16 @@ const App: React.FC = ({ controller.signal, ) - if (!controller.signal.aborted) { - if (typeof res === "string") { - setResultForIndex(res, index) - } else { - setResultForIndex(res.message, index) - setAdditionalDataList((prev) => { - const newDataList = [...prev] - newDataList[index] = { - cost: res.cost, - latency: res.latency, - usage: res.usage, - } - return newDataList - }) - } + // check if res is an object or string + if (typeof res === "string") { + setResultForIndex(res, index) + } else { + setResultForIndex(res.message, index) + setAdditionalDataList((prev) => { + const newDataList = [...prev] + newDataList[index] = {cost: res.cost, latency: res.latency, usage: res.usage} + return newDataList + }) } } catch (e) { if (!controller.signal.aborted) { @@ -440,7 +440,7 @@ const App: React.FC = ({ const funcs: Function[] = [] rootRef.current ?.querySelectorAll("[data-cy=testview-input-parameters-run-button]") - .forEach((btn, index) => funcs.push(() => handleRun(index))) + .forEach((btn) => funcs.push(() => (btn as HTMLButtonElement).click())) batchExecute(funcs) } diff --git a/agenta-web/src/lib/services/api.ts b/agenta-web/src/lib/services/api.ts index cd5a438605..807be1cf90 100644 --- a/agenta-web/src/lib/services/api.ts +++ b/agenta-web/src/lib/services/api.ts @@ -88,12 +88,14 @@ export async function callVariant( ) { const isChatVariant = Array.isArray(chatMessages) && chatMessages.length > 0 // Separate input parameters into two dictionaries based on the 'input' property - const mainInputParams: Record = {} - const secondaryInputParams: Record = {} + const mainInputParams: Record = {} // Parameters with input = true + const secondaryInputParams: Record = {} // Parameters with input = false for (let key of Object.keys(inputParametersDict)) { const paramDefinition = inputParamDefinition.find((param) => param.name === key) + // If parameter definition is found and its 'input' property is false, + // then it goes to 'secondaryInputParams', otherwise to 'mainInputParams' if (paramDefinition && !paramDefinition.input) { secondaryInputParams[key] = inputParametersDict[key] } else { @@ -105,7 +107,7 @@ export async function callVariant( const optParams = optionalParameters .filter((param) => param.default) - .filter((param) => param.type !== "object") + .filter((param) => param.type !== "object") // remove dicts from optional parameters .reduce((acc: any, param) => { acc[param.name] = param.default return acc From 2429d1c1e0dd9bf5aca36721bf8abb3581788238 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Thu, 11 Jan 2024 13:23:41 +0100 Subject: [PATCH 173/267] conditionally change run all button to cancel all when clicked --- .../components/Playground/Views/TestView.tsx | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/agenta-web/src/components/Playground/Views/TestView.tsx b/agenta-web/src/components/Playground/Views/TestView.tsx index b0317321f7..e81618ba74 100644 --- a/agenta-web/src/components/Playground/Views/TestView.tsx +++ b/agenta-web/src/components/Playground/Views/TestView.tsx @@ -290,6 +290,7 @@ const App: React.FC = ({ >(testList.map(() => ({cost: null, latency: null, usage: null}))) const abortControllersRef = useRef([]) + const [isRunningAll, setIsRunningAll] = useState(false) useEffect(() => { return () => { @@ -429,7 +430,15 @@ const App: React.FC = ({ }) } - const handleRunAll = () => { + const handleRunAll = async () => { + if (isRunningAll) { + handleCancelAll() + setIsRunningAll(false) + return + } + + setIsRunningAll(true) + abortControllersRef.current.forEach((controller) => controller && controller.abort()) const newAbortControllers = Array(testList.length) @@ -442,7 +451,13 @@ const App: React.FC = ({ ?.querySelectorAll("[data-cy=testview-input-parameters-run-button]") .forEach((btn) => funcs.push(() => (btn as HTMLButtonElement).click())) - batchExecute(funcs) + try { + await batchExecute(funcs) + } catch (e) { + setIsRunningAll(false) + } finally { + setIsRunningAll(false) + } } const handleAddRow = () => { @@ -509,17 +524,25 @@ const App: React.FC = ({ - - + {!isRunningAll ? ( + + ) : ( + + )}
From 086bb76f5f3886f31f04a9c5174a5bc86155d5c8 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 11 Jan 2024 14:16:19 +0100 Subject: [PATCH 174/267] hotfix --- agenta-backend/agenta_backend/routers/configs_router.py | 2 +- agenta-cli/agenta/config.py | 5 ++++- agenta-cli/pyproject.toml | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/agenta-backend/agenta_backend/routers/configs_router.py b/agenta-backend/agenta_backend/routers/configs_router.py index 0f0704b762..c6a2754f40 100644 --- a/agenta-backend/agenta_backend/routers/configs_router.py +++ b/agenta-backend/agenta_backend/routers/configs_router.py @@ -53,7 +53,7 @@ async def save_config( ) else: raise HTTPException( - status_code=400, + status_code=200, detail="Config name already exists. Please use a different name or set overwrite to True.", ) else: diff --git a/agenta-cli/agenta/config.py b/agenta-cli/agenta/config.py index 10e5e1965a..167ccc86d0 100644 --- a/agenta-cli/agenta/config.py +++ b/agenta-cli/agenta/config.py @@ -1,4 +1,7 @@ -from pydantic.v1 import BaseSettings +try: + from pydantic.v1 import BaseSettings # type: ignore +except ImportError: + from pydantic import BaseSettings # type: ignore import os import toml diff --git a/agenta-cli/pyproject.toml b/agenta-cli/pyproject.toml index 36618f6acb..ee2acb1777 100644 --- a/agenta-cli/pyproject.toml +++ b/agenta-cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "agenta" -version = "0.7.0" +version = "0.7.1" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" authors = ["Mahmoud Mabrouk "] @@ -19,7 +19,7 @@ keywords = ["LLMOps", "LLM", "evaluation", "prompt engineering"] python = "^3.9" docker = "^6.1.1" click = "^8.1.3" -fastapi = ">=0.95.1" +fastapi = ">=0.96.1" toml = "^0.10.2" questionary = "^1.10.0" ipdb = ">=0.13" @@ -27,7 +27,7 @@ python-dotenv = "^1.0.0" python-multipart = "^0.0.6" importlib-metadata = "^6.7.0" posthog = "^3.1.0" -pydantic = ">=2.0" +pydantic = "1.10.13" [tool.poetry.dev-dependencies] pytest = "^6.2" From 194d2893eeb9310efa91ac797b621e54f1f42c76 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Thu, 11 Jan 2024 15:31:15 +0100 Subject: [PATCH 175/267] reusable antd result component added --- .../ResultComponent/ResultComponent.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 agenta-web/src/components/ResultComponent/ResultComponent.tsx diff --git a/agenta-web/src/components/ResultComponent/ResultComponent.tsx b/agenta-web/src/components/ResultComponent/ResultComponent.tsx new file mode 100644 index 0000000000..93bbf0bfbd --- /dev/null +++ b/agenta-web/src/components/ResultComponent/ResultComponent.tsx @@ -0,0 +1,16 @@ +import {Result, Spin} from "antd" +import {ResultStatusType} from "antd/es/result" +import React from "react" + +interface ResultComponentProps { + status: ResultStatusType + title: string + subtitle?: string + spinner?: boolean +} + +const ResultComponent: React.FC = ({status, title, subtitle, spinner}) => { + return } /> +} + +export default ResultComponent From f43ce1a69d11a3a834561ec16ff30a8f474f759f Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Thu, 11 Jan 2024 15:32:23 +0100 Subject: [PATCH 176/267] improve and fix visibility in dark mode --- .../src/components/AppSelector/AppSelector.tsx | 10 ++++------ agenta-web/src/components/Playground/Playground.tsx | 13 +++++++++++-- .../src/pages/apps/[app_id]/endpoints/index.tsx | 13 ++++++++----- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/agenta-web/src/components/AppSelector/AppSelector.tsx b/agenta-web/src/components/AppSelector/AppSelector.tsx index cca4ebfe6d..5a953d04f1 100644 --- a/agenta-web/src/components/AppSelector/AppSelector.tsx +++ b/agenta-web/src/components/AppSelector/AppSelector.tsx @@ -1,11 +1,10 @@ import {useState, useEffect, useMemo} from "react" import {useRouter} from "next/router" import {PlusOutlined} from "@ant-design/icons" -import {Input, Modal, ConfigProvider, theme, Spin, Card, Button, notification, Divider} from "antd" +import {Input, Modal, ConfigProvider, theme, Card, Button, notification, Divider} from "antd" import AppCard from "./AppCard" import {Template, GenericObject} from "@/lib/Types" import {useAppTheme} from "../Layout/ThemeContextProvider" -import {CloseCircleFilled} from "@ant-design/icons" import TipsAndFeatures from "./TipsAndFeatures" import Welcome from "./Welcome" import {getApikeys, isAppNameInputValid, isDemo} from "@/lib/helpers/utils" @@ -24,6 +23,7 @@ import {useAppsData} from "@/contexts/app.context" import {useProfileData} from "@/contexts/profile.context" import CreateAppStatusModal from "./modals/CreateAppStatusModal" import {usePostHogAg} from "@/hooks/usePostHogAg" +import ResultComponent from "../ResultComponent/ResultComponent" type StyleProps = { themeMode: "dark" | "light" @@ -260,13 +260,11 @@ const AppSelector: React.FC = () => {
{isLoading ? (
- -

loading...

+
) : error ? (
- -

failed to load

+
) : Array.isArray(apps) && apps.length ? ( <> diff --git a/agenta-web/src/components/Playground/Playground.tsx b/agenta-web/src/components/Playground/Playground.tsx index 4a8bcc3bc2..ca938015c4 100644 --- a/agenta-web/src/components/Playground/Playground.tsx +++ b/agenta-web/src/components/Playground/Playground.tsx @@ -15,6 +15,7 @@ import {arrayMove, SortableContext, horizontalListSortingStrategy} from "@dnd-ki import DraggableTabNode from "../DraggableTabNode/DraggableTabNode" import {useLocalStorage} from "usehooks-ts" import TestContextProvider from "./TestContextProvider" +import ResultComponent from "../ResultComponent/ResultComponent" const Playground: React.FC = () => { const router = useRouter() @@ -162,8 +163,16 @@ const Playground: React.FC = () => { (newRoute) => !newRoute.includes("playground"), ) - if (isError) return
failed to load variants for app {appId}
- if (isLoading) return
loading variants...
+ if (isError) + return ( + + ) + if (isLoading) + return /** * Called when the variant is saved for the first time to the backend diff --git a/agenta-web/src/pages/apps/[app_id]/endpoints/index.tsx b/agenta-web/src/pages/apps/[app_id]/endpoints/index.tsx index 4f900d2e00..65816b4e43 100644 --- a/agenta-web/src/pages/apps/[app_id]/endpoints/index.tsx +++ b/agenta-web/src/pages/apps/[app_id]/endpoints/index.tsx @@ -2,6 +2,7 @@ import cURLCode from "@/code_snippets/endpoints/curl" import pythonCode from "@/code_snippets/endpoints/python" import tsCode from "@/code_snippets/endpoints/typescript" import DynamicCodeBlock from "@/components/DynamicCodeBlock/DynamicCodeBlock" +import ResultComponent from "@/components/ResultComponent/ResultComponent" import {Environment, GenericObject, Parameter, Variant} from "@/lib/Types" import {useVariant} from "@/lib/hooks/useVariant" import {fetchEnvironments, fetchVariants, getAppContainerURL} from "@/lib/services/api" @@ -129,19 +130,21 @@ export default function VariantEndpoint() { } if (isVariantsError) { - return
Failed to load variants
+ return } if (isVariantsLoading) { - return
Loading variants...
+ return } if (!variant) { - return
No variant available
+ return } if (isLoading) { - return
Loading variant...
+ return } if (isError) { - return
{error?.message || "Error loading variant"}
+ return ( + + ) } const params = createParams(inputParams, selectedEnvironment?.name || "none", "add_a_value") From fa02500d46f664d41f9122342c2a526b41ee4f14 Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 11 Jan 2024 15:54:08 +0100 Subject: [PATCH 177/267] Update - added logic to migrate old evaluation scenario to new auto/human evaluation scenario --- .../20240110165900_evaluations_revamp.py | 456 ++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py new file mode 100644 index 0000000000..f972784806 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -0,0 +1,456 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + + +from pydantic import BaseModel, Field +from beanie import free_fall_migration, Document, Link, PydanticObjectId + + +class OrganizationDB(Document): + name: str = Field(default="agenta") + description: str = Field(default="") + type: Optional[str] + owner: str # user id + members: Optional[List[PydanticObjectId]] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "organizations" + + +class UserDB(Document): + uid: str = Field(default="0", unique=True, index=True) + username: str = Field(default="agenta") + email: str = Field(default="demo@agenta.ai", unique=True) + organizations: Optional[List[PydanticObjectId]] = [] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "users" + + +class AppDB(Document): + app_name: str + organization: Link[OrganizationDB] + user: Link[UserDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "app_db" + + +class TestSetDB(Document): + name: str + app: Link[AppDB] + csvdata: List[Dict[str, str]] + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "testsets" + + +class EvaluatorConfigDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + name: str + evaluator_key: str + settings_values: Optional[Dict[str, Any]] = None + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluators_configs" + + +class Result(BaseModel): + type: str + value: Any + + +class EvaluationScenarioResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class AggregatedResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class EvaluationScenarioInputDB(BaseModel): + name: str + type: str + value: str + + +class EvaluationScenarioOutputDB(BaseModel): + type: str + value: Any + + +class HumanEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class HumanEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class HumanEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + variants: List[PydanticObjectId] + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "human_evaluations" + + +class HumanEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[HumanEvaluationDB] + inputs: List[HumanEvaluationScenarioInput] + outputs: List[HumanEvaluationScenarioOutput] + vote: Optional[str] + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "human_evaluations_scenarios" + + +class EvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str = Field(default="EVALUATION_INITIALIZED") + testset: Link[TestSetDB] + variant: PydanticObjectId + evaluators_configs: List[PydanticObjectId] + aggregated_results: List[AggregatedResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluations" + + +class EvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[EvaluationDB] + variant_id: PydanticObjectId + inputs: List[EvaluationScenarioInputDB] + outputs: List[EvaluationScenarioOutputDB] + correct_answer: Optional[str] + is_pinned: Optional[bool] + note: Optional[str] + evaluators_configs: List[PydanticObjectId] + results: List[EvaluationScenarioResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluation_scenarios" + + +class OldEvaluationTypeSettings(BaseModel): + similarity_threshold: Optional[float] + regex_pattern: Optional[str] + regex_should_match: Optional[bool] + webhook_url: Optional[str] + llm_app_prompt_template: Optional[str] + custom_code_evaluation_id: Optional[str] + evaluation_prompt_template: Optional[str] + + +class OldEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class OldEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class OldEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + evaluation_type_settings: OldEvaluationTypeSettings + variants: List[PydanticObjectId] + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluations" + + +class OldEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[OldEvaluationDB] + inputs: List[OldEvaluationScenarioInput] + outputs: List[OldEvaluationScenarioOutput] # EvaluationScenarioOutput + vote: Optional[str] + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "evaluation_scenarios" + + +class OldCustomEvaluationDB(Document): + evaluation_name: str + python_code: str + app: Link[AppDB] + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "custom_evaluations" + + +def modify_app_id_store( + app_id: str, + variant_ids: str, + evaluation_type: str, + app_keyvalue_store: Dict[str, Dict[str, List[str]]], +): + app_id_store = app_keyvalue_store.get(app_id, None) + if not app_id_store: + app_keyvalue_store[app_id] = {"variant_ids": [], "evaluation_types": []} + app_id_store = app_keyvalue_store[app_id] + + app_id_store_variant_ids = list(app_id_store["variant_ids"]) + if variant_ids not in list(app_id_store["variant_ids"]): + app_id_store_variant_ids.extend(variant_ids) + app_id_store["variant_ids"] = list(set(app_id_store_variant_ids)) + + app_id_store_evaluation_types = list(app_id_store["evaluation_types"]) + if evaluation_type not in app_id_store_evaluation_types: + app_id_store_evaluation_types.append(evaluation_type) + app_id_store["evaluation_types"] = list(set(app_id_store_evaluation_types)) + + +class Forward: + @free_fall_migration( + document_models=[ + AppDB, + UserDB, + OrganizationDB, + TestSetDB, + OldEvaluationDB, + EvaluatorConfigDB, + HumanEvaluationDB, + EvaluationDB, + OldCustomEvaluationDB, + ] + ) + async def migrate_old_evaluation_to_new_evaluation(self, session): + # STEP 1: + # Create a key-value store that saves all the variants & evaluation types for a particular app id + # Example: {"app_id": {"evaluation_types": ["string", "string"], "variant_ids": ["string", "string"]}} + app_keyvalue_store = {} + old_evaluations = await OldEvaluationDB.find(fetch_links=True).to_list() + for old_eval in old_evaluations: + app_id = old_eval.app.id + variant_ids = [str(variant_id) for variant_id in old_eval.variants] + evaluation_type = old_eval.evaluation_type + modify_app_id_store( + str(app_id), variant_ids, evaluation_type, app_keyvalue_store + ) + + # STEP 2: + # Loop through the app_id key-store to create evaluator configs + # based on the evaluation types available + for app_id, app_id_store in app_keyvalue_store.items(): + app_evaluator_configs: List[EvaluatorConfigDB] = [] + for evaluation_type in app_id_store[ + "evaluation_types" + ]: # the values in this case are the evaluation type + custom_code_evaluations = await OldCustomEvaluationDB.find( + OldCustomEvaluationDB.app == PydanticObjectId(app_id) + ).to_list() + if evaluation_type == "custom_code_run": + for custom_code_evaluation in custom_code_evaluations: + eval_config = EvaluatorConfigDB( + app=PydanticObjectId(app_id), + organization=old_eval.organization.id, + user=old_eval.user.id, + name=f"{old_eval.app.app_name}_{old_eval.evaluation_type}", + evaluator_key=f"auto_{evaluation_type}", + settings_values={} + if custom_code_evaluation is None + else {"code": custom_code_evaluation.python_code}, + ) + await eval_config.create(session=session) + app_evaluator_configs.append(eval_config) + + if evaluation_type != "custom_code_run": + eval_config = EvaluatorConfigDB( + app=PydanticObjectId(app_id), + organization=old_eval.organization.id, + user=old_eval.user.id, + name=f"{old_eval.app.app_name}_{old_eval.evaluation_type}", + evaluator_key=evaluation_type, + settings_values={}, + ) + await eval_config.create(session=session) + app_evaluator_configs.append(eval_config) + + # STEP 3 (a): + # Retrieve evaluator configs for app id + auto_evaluator_configs: List[PydanticObjectId] = [] + for evaluator_config in app_evaluator_configs: + # In the case where the evaluator key is not a human evaluator, + # Append the evaluator config id in the list of auto evaluator configs + if evaluator_config.evaluator_key not in [ + "human_a_b_testing", + "single_model_test", + ]: + auto_evaluator_configs.append(evaluator_config.id) + + # STEP 3 (b): + # In the case where the evaluator key is a human evaluator, + # Proceed to create the human evaluation with the evaluator config + for evaluator_config in app_evaluator_configs: + if evaluator_config.evaluator_key in [ + "human_a_b_testing", + "single_model_test", + ]: + new_eval = HumanEvaluationDB( + app=PydanticObjectId(app_id), + organization=old_eval.organization.id, + user=old_eval.user.id, + status=old_eval.status, + evaluation_type=evaluator_config.evaluator_key, + variants=app_id_store["variant_ids"], + testset=old_eval.testset.id, + ) + await new_eval.create(session=session) # replace(session=session) + + # STEP 3 (c): + # Proceed to create a single evaluation for every variant in the app_id_store + # with the auto_evaluator_configs + if auto_evaluator_configs is not None: + for variant in app_id_store["variant_ids"]: + new_eval = EvaluationDB( + app=PydanticObjectId(app_id), + organization=old_eval.organization.id, + user=old_eval.user.id, + status=old_eval.status, + testset=old_eval.testset.id, + variant=variant, + evaluators_configs=auto_evaluator_configs, + aggregated_results=[], + ) + await new_eval.create(session=session) + + @free_fall_migration( + document_models=[ + AppDB, + OrganizationDB, + UserDB, + TestSetDB, + OldEvaluationDB, + OldEvaluationScenarioDB, + EvaluationScenarioDB, + HumanEvaluationScenarioDB, + ] + ) + async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, session): + old_scenarios = await OldEvaluationScenarioDB.find(fetch_links=True).to_list() + for old_scenario in old_scenarios: + if old_scenario.evaluation.evaluation_type in [ + "human_a_b_testing", + "single_model_test", + ]: + scenario_inputs = [ + HumanEvaluationScenarioInput( + input_name=input.input_name, + input_value=input.input_value, + ) + for input in old_scenario.inputs + ] + scenario_outputs = [ + HumanEvaluationScenarioOutput( + variant_id=output.variant_id, + variant_output=output.variant_output, + ) + for output in old_scenario.outputs + ] + new_scenario = HumanEvaluationScenarioDB( + user=old_scenario.user.id, + organization=old_scenario.organization.id, + evaluation=old_scenario.evaluation.id, + inputs=scenario_inputs, + outputs=scenario_outputs, + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + vote=old_scenario.vote, + score=old_scenario.score, + ) + await new_scenario.insert(session=session) + else: + new_scenario = EvaluationScenarioDB( + user=old_scenario.user.id, + organization=old_scenario.organization.id, + evaluation=old_scenario.evaluation.id, + variant_id=old_scenario.evaluation.variants[0], + inputs=[ + EvaluationScenarioInputDB( + name=input.input_name, + type=type(input.input_value).__name__, + value=input.input_value, + ) + for input in old_scenario.inputs + ], + outputs=[ + EvaluationScenarioOutputDB( + type=type(output.variant_output).__name__, + value=output.variant_output, + ) + for output in old_scenario.outputs + ], + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + evaluators_configs=[], + results=[], + ) + await new_scenario.insert(session=session) + + +class Backward: + ... From aa45abdfa243b89297eac286f831af712f97f47f Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 11 Jan 2024 15:55:19 +0100 Subject: [PATCH 178/267] :art: Format - ran black --- .../20240110001454_initial_migration.py | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py b/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py index ced0493ee3..ef06b5c11b 100644 --- a/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py +++ b/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py @@ -250,7 +250,7 @@ class Forward: VariantBaseDB, ConfigDB, AppVariantDB, - OldAppVariantDB + OldAppVariantDB, ] ) async def change_app_variant_fields( @@ -259,20 +259,23 @@ async def change_app_variant_fields( output_document.base = input_document.bases output_document.config = input_document.configs - @iterative_migration(document_models=[ + @iterative_migration( + document_models=[ OrganizationDB, UserDB, AppDB, TestSetDB, EvaluationDB, - OldEvaluationDB - ]) + OldEvaluationDB, + ] + ) async def rename_evaluation_fields( self, input_document: OldEvaluationDB, output_document: EvaluationDB ): output_document.testset = input_document.testsets - @iterative_migration(document_models=[ + @iterative_migration( + document_models=[ OrganizationDB, UserDB, AppDB, @@ -280,8 +283,9 @@ async def rename_evaluation_fields( EvaluationDB, OldEvaluationDB, EvaluationScenarioDB, - OldEvaluationScenarioDB - ]) + OldEvaluationScenarioDB, + ] + ) async def rename_evaluation_scenarios_fields( self, input_document: OldEvaluationScenarioDB, @@ -299,7 +303,7 @@ class Backward: VariantBaseDB, ConfigDB, AppVariantDB, - OldAppVariantDB + OldAppVariantDB, ] ) async def change_app_variant_fields( @@ -308,20 +312,23 @@ async def change_app_variant_fields( output_document.bases = input_document.base output_document.configs = input_document.config - @iterative_migration(document_models=[ + @iterative_migration( + document_models=[ OrganizationDB, UserDB, AppDB, TestSetDB, EvaluationDB, - OldEvaluationDB - ]) + OldEvaluationDB, + ] + ) async def rename_evaluation_fields( self, input_document: EvaluationDB, output_document: OldEvaluationDB ): output_document.testsets = input_document.testset - @iterative_migration(document_models=[ + @iterative_migration( + document_models=[ OrganizationDB, UserDB, AppDB, @@ -329,8 +336,9 @@ async def rename_evaluation_fields( EvaluationDB, OldEvaluationDB, EvaluationScenarioDB, - OldEvaluationScenarioDB - ]) + OldEvaluationScenarioDB, + ] + ) async def rename_evaluation_scenarios_fields( self, input_document: EvaluationScenarioDB, From 090002a8ecb79129707e85d61966f3cca9937432 Mon Sep 17 00:00:00 2001 From: Abram Date: Thu, 11 Jan 2024 16:32:43 +0100 Subject: [PATCH 179/267] Update - modified evaluations revamp migration logic --- .../migrations/20240110165900_evaluations_revamp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index f972784806..b71f41152a 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -382,9 +382,11 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): OrganizationDB, UserDB, TestSetDB, + EvaluationDB, OldEvaluationDB, OldEvaluationScenarioDB, EvaluationScenarioDB, + HumanEvaluationDB, HumanEvaluationScenarioDB, ] ) From 2e7fc64aa6a4ba81774552e6aa3d45198f58f071 Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Thu, 11 Jan 2024 19:38:29 +0100 Subject: [PATCH 180/267] fix for the new lm app response --- agenta-backend/agenta_backend/services/llm_apps_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/services/llm_apps_service.py b/agenta-backend/agenta_backend/services/llm_apps_service.py index 3caa069dbe..f040b85e21 100644 --- a/agenta-backend/agenta_backend/services/llm_apps_service.py +++ b/agenta-backend/agenta_backend/services/llm_apps_service.py @@ -77,7 +77,9 @@ async def invoke_app( url, json=payload, timeout=httpx.Timeout(timeout=5, read=None, write=5) ) response.raise_for_status() - return AppOutput(output=response.json(), status="success") + + lm_app_response = response.json() + return AppOutput(output=lm_app_response["message"], status="success") async def run_with_retry( From a4fd1c0f2e58297a0ce5b1310c7c151d7e3d594f Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 11 Jan 2024 21:26:58 +0100 Subject: [PATCH 181/267] fixed issue with inputs in eval --- .../services/llm_apps_service.py | 25 ++++++++++++++----- .../agenta_backend/tasks/evaluations.py | 13 +++++++--- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/agenta-backend/agenta_backend/services/llm_apps_service.py b/agenta-backend/agenta_backend/services/llm_apps_service.py index f040b85e21..67115ad5ef 100644 --- a/agenta-backend/agenta_backend/services/llm_apps_service.py +++ b/agenta-backend/agenta_backend/services/llm_apps_service.py @@ -32,10 +32,18 @@ async def make_payload( for param in openapi_parameters: if param["type"] == "input": payload[param["name"]] = datapoint.get(param["name"], "") - elif param["type"] == "dict": - for input_name in parameters[param["name"]]: - input_name_ = input_name["name"] - inputs_dict[input_name_] = datapoint.get(input_name_, "") + elif param["type"] == "dict": # in case of dynamic inputs (as in our templates) + # let's get the list of the dynamic inputs + if ( + param["name"] in parameters + ): # in case we have modified in the playground the default list of inputs (e.g. country_name) + input_names = [_["name"] for _ in parameters[param["name"]]] + else: # otherwise we use the default from the openapi + input_names = param["default"] + # now we put them in a dict which we would put under "inputs" in the payload + + for input_name in input_names: + inputs_dict[input_name] = datapoint.get(input_name, "") elif param["type"] == "messages": # TODO: Right now the FE is saving chats always under the column name chats. The whole logic for handling chats and dynamic inputs is convoluted and needs rework in time. payload[param["name"]] = json.loads(datapoint.get("chat", "")) @@ -219,8 +227,13 @@ async def get_parameters_from_openapi(uri: str) -> List[Dict]: parameters = [] for name, param in properties.items(): - parameters.append({"name": name, "type": param.get("x-parameter", "input")}) - + parameters.append( + { + "name": name, + "type": param.get("x-parameter", "input"), + "default": param.get("default", []), + } + ) return parameters diff --git a/agenta-backend/agenta_backend/tasks/evaluations.py b/agenta-backend/agenta_backend/tasks/evaluations.py index abef76ea22..5b0b861389 100644 --- a/agenta-backend/agenta_backend/tasks/evaluations.py +++ b/agenta-backend/agenta_backend/tasks/evaluations.py @@ -261,9 +261,16 @@ def get_app_inputs(app_variant_parameters, openapi_parameters) -> List[Dict[str, for param in openapi_parameters: if param["type"] == "input": list_inputs.append({"name": param["name"], "type": "input"}) - elif param["type"] == "dict": - for input_name in app_variant_parameters[param["name"]]: - list_inputs.append({"name": input_name["name"], "type": "dict_input"}) + elif param["type"] == "dict": # in case of dynamic inputs (as in our templates) + # let's get the list of the dynamic inputs + if ( + param["name"] in app_variant_parameters + ): # in case we have modified in the playground the default list of inputs (e.g. country_name) + input_names = [_["name"] for _ in app_variant_parameters[param["name"]]] + else: # otherwise we use the default from the openapi + input_names = param["default"] + for input_name in input_names: + list_inputs.append({"name": input_name, "type": "dict_input"}) elif param["type"] == "messages": list_inputs.append({"name": param["name"], "type": "messages"}) elif param["type"] == "file_url": From dd6d652d468400b0f07c858ea183dce3090c5e23 Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Fri, 12 Jan 2024 10:49:54 +0100 Subject: [PATCH 182/267] fix ai critique --- .../agenta_backend/services/evaluation_service.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/services/evaluation_service.py b/agenta-backend/agenta_backend/services/evaluation_service.py index 15efcd0310..127607b849 100644 --- a/agenta-backend/agenta_backend/services/evaluation_service.py +++ b/agenta-backend/agenta_backend/services/evaluation_service.py @@ -508,7 +508,11 @@ def evaluate_with_ai_critique( Returns: str: returns an evaluation """ - llm = OpenAI(openai_api_key=open_ai_key, temperature=temperature) + llm = OpenAI( + openai_api_key=open_ai_key, + model="gpt-3.5-turbo-instruct", + temperature=temperature, + ) input_variables = [] From 623e97d85f304a8fd38ccd542b0b6765deeebb2f Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 12 Jan 2024 12:40:13 +0100 Subject: [PATCH 183/267] Feat - created migration file to change odmantic reference to link --- .../20240110165900_evaluations_revamp.py | 2 +- ...20721_change_odmantic_reference_to_link.py | 326 ++++++++++++++++++ 2 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 agenta-backend/agenta_backend/migrations/20240112120721_change_odmantic_reference_to_link.py diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index b71f41152a..9daadf3af2 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -455,4 +455,4 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi class Backward: - ... + pass diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_change_odmantic_reference_to_link.py b/agenta-backend/agenta_backend/migrations/20240112120721_change_odmantic_reference_to_link.py new file mode 100644 index 0000000000..7aaca4b5e3 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/20240112120721_change_odmantic_reference_to_link.py @@ -0,0 +1,326 @@ +from beanie import iterative_migration, Document, Link + +from uuid import uuid4 +from datetime import datetime +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field +from beanie import Document, Link, PydanticObjectId + + +class APIKeyDB(Document): + prefix: str + hashed_key: str + user_id: str + rate_limit: int = Field(default=0) + hidden: Optional[bool] = Field(default=False) + expiration_date: Optional[datetime] + created_at: Optional[datetime] = datetime.utcnow() + updated_at: Optional[datetime] + + class Settings: + name = "api_keys" + + +class InvitationDB(BaseModel): + token: str = Field(unique=True) + email: str + expiration_date: datetime = Field(default="0") + used: bool = False + + +class OrganizationDB(Document): + name: str = Field(default="agenta") + description: str = Field(default="") + type: Optional[str] + owner: str # user id + members: Optional[List[PydanticObjectId]] + invitations: Optional[List[InvitationDB]] = [] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "organizations" + + +class UserDB(Document): + uid: str = Field(default="0", unique=True, index=True) + username: str = Field(default="agenta") + email: str = Field(default="demo@agenta.ai", unique=True) + organizations: Optional[List[PydanticObjectId]] = [] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "users" + + +class ImageDB(Document): + """Defines the info needed to get an image and connect it to the app variant""" + + type: Optional[str] = Field(default="image") + template_uri: Optional[str] + docker_id: Optional[str] = Field(index=True) + tags: Optional[str] + deletable: bool = Field(default=True) + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "docker_images" + + +class AppDB(Document): + app_name: str + organization: Link[OrganizationDB] + user: Link[UserDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "app_db" + + +class DeploymentDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + container_name: Optional[str] + container_id: Optional[str] + uri: Optional[str] + status: str + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "deployments" + + +class VariantBaseDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + base_name: str + image: Link[ImageDB] + deployment: Optional[PydanticObjectId] # Link to deployment + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "bases" + + +class ConfigVersionDB(BaseModel): + version: int + parameters: Dict[str, Any] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + +class ConfigDB(Document): + config_name: str + current_version: int = Field(default=1) + parameters: Dict[str, Any] = Field(default=dict) + version_history: List[ConfigVersionDB] = Field(default=[]) + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "configs" + + +class AppVariantDB(Document): + app: Link[AppDB] + variant_name: str + image: Link[ImageDB] + user: Link[UserDB] + organization: Link[OrganizationDB] + parameters: Dict[str, Any] = Field(default=dict) # TODO: deprecated. remove + previous_variant_name: Optional[str] # TODO: deprecated. remove + base_name: Optional[str] + base: Link[VariantBaseDB] + config_name: Optional[str] + config: Link[ConfigDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + is_deleted: bool = Field( # TODO: deprecated. remove + default=False + ) # soft deletion for using the template variants + + class Settings: + name = "app_variants" + + +class AppEnvironmentDB(Document): + app: Link[AppDB] + name: str + user: Link[UserDB] + organization: Link[OrganizationDB] + deployed_app_variant: Optional[PydanticObjectId] + deployment: Optional[PydanticObjectId] # reference to deployment + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "app_environment_db" + + +class TemplateDB(Document): + type: Optional[str] = Field(default="image") + template_uri: Optional[str] + tag_id: Optional[int] + name: str = Field(unique=True) # tag name of image + repo_name: Optional[str] + title: str + description: str + size: Optional[int] + digest: Optional[str] # sha256 hash of image digest + last_pushed: Optional[datetime] + + class Settings: + name = "templates" + + +class TestSetDB(Document): + name: str + app: Link[AppDB] + csvdata: List[Dict[str, str]] + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "testsets" + + +class EvaluatorConfigDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + name: str + evaluator_key: str + settings_values: Optional[Dict[str, Any]] = None + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluators_configs" + + +class Result(BaseModel): + type: str + value: Any + + +class EvaluationScenarioResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class AggregatedResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class EvaluationScenarioInputDB(BaseModel): + name: str + type: str + value: str + + +class EvaluationScenarioOutputDB(BaseModel): + type: str + value: Any + + +class HumanEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class HumanEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class HumanEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + variants: List[PydanticObjectId] + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "human_evaluations" + + +class HumanEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[HumanEvaluationDB] + inputs: List[HumanEvaluationScenarioInput] + outputs: List[HumanEvaluationScenarioOutput] + vote: Optional[str] + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "human_evaluations_scenarios" + + +class EvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str = Field(default="EVALUATION_INITIALIZED") + testset: Link[TestSetDB] + variant: PydanticObjectId + evaluators_configs: List[PydanticObjectId] + aggregated_results: List[AggregatedResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluations" + + +class EvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[EvaluationDB] + variant_id: PydanticObjectId + inputs: List[EvaluationScenarioInputDB] + outputs: List[EvaluationScenarioOutputDB] + correct_answer: Optional[str] + is_pinned: Optional[bool] + note: Optional[str] + evaluators_configs: List[PydanticObjectId] + results: List[EvaluationScenarioResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluation_scenarios" + + +class Forward: + @iterative_migration(document_models=[OrganizationDB, UserDB, ImageDB]) + async def rename_image_db_reference_to_link(self, input_document: ImageDB, output_document: ImageDB): + output_document.user = input_document.user + +class Backward: + @iterative_migration(document_models=[OrganizationDB, UserDB, ImageDB]) + async def rename_image_db_reference_to_link(self, input_document: ImageDB, output_document: ImageDB): + output_document.user = input_document.user + From b7833ed9796c4b715ba369e8300fc40d17f95f9b Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 12 Jan 2024 13:30:55 +0100 Subject: [PATCH 184/267] Update - conclude iterative migraiton logic to change odmantic reference to link --- .../20240110001454_initial_migration.py | 12 ++-- ...20721_change_odmantic_reference_to_link.py | 62 +++++++++++++++++-- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py b/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py index ef06b5c11b..b44c47246d 100644 --- a/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py +++ b/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py @@ -167,9 +167,9 @@ class OldEvaluationDB(Document): organization: Link[OrganizationDB] user: Link[UserDB] status: str - evaluation_type: str - evaluation_type_settings: OldEvaluationTypeSettings - variants: List[PydanticObjectId] + evaluation_type: Optional[str] + evaluation_type_settings: Optional[OldEvaluationTypeSettings] + variants: Optional[List[PydanticObjectId]] testsets: Link[TestSetDB] created_at: Optional[datetime] = Field(default=datetime.utcnow()) updated_at: Optional[datetime] = Field(default=datetime.utcnow()) @@ -183,9 +183,9 @@ class EvaluationDB(Document): organization: Link[OrganizationDB] user: Link[UserDB] status: str - evaluation_type: str - evaluation_type_settings: OldEvaluationTypeSettings - variants: List[PydanticObjectId] + evaluation_type: Optional[str] + evaluation_type_settings: Optional[OldEvaluationTypeSettings] + variants: Optional[List[PydanticObjectId]] testset: Link[TestSetDB] created_at: Optional[datetime] = Field(default=datetime.utcnow()) updated_at: Optional[datetime] = Field(default=datetime.utcnow()) diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_change_odmantic_reference_to_link.py b/agenta-backend/agenta_backend/migrations/20240112120721_change_odmantic_reference_to_link.py index 7aaca4b5e3..12518af42c 100644 --- a/agenta-backend/agenta_backend/migrations/20240112120721_change_odmantic_reference_to_link.py +++ b/agenta-backend/agenta_backend/migrations/20240112120721_change_odmantic_reference_to_link.py @@ -316,11 +316,65 @@ class Settings: class Forward: @iterative_migration(document_models=[OrganizationDB, UserDB, ImageDB]) - async def rename_image_db_reference_to_link(self, input_document: ImageDB, output_document: ImageDB): + async def rename_image_db_reference_to_link( + self, input_document: ImageDB, output_document: ImageDB + ): output_document.user = input_document.user -class Backward: - @iterative_migration(document_models=[OrganizationDB, UserDB, ImageDB]) - async def rename_image_db_reference_to_link(self, input_document: ImageDB, output_document: ImageDB): + @iterative_migration(document_models=[OrganizationDB, UserDB, AppDB]) + async def rename_app_db_reference_to_link( + self, input_document: AppDB, output_document: AppDB + ): + output_document.user = input_document.user + output_document.organization = input_document.organization + + @iterative_migration(document_models=[OrganizationDB, UserDB, AppDB, DeploymentDB]) + async def rename_deployment_db_reference_to_link( + self, input_document: DeploymentDB, output_document: DeploymentDB + ): + output_document.app = input_document.app + output_document.user = input_document.user + output_document.organization = input_document.organization + + @iterative_migration( + document_models=[OrganizationDB, UserDB, AppDB, ImageDB, VariantBaseDB] + ) + async def rename_variant_base_db_reference_to_link( + self, input_document: VariantBaseDB, output_document: VariantBaseDB + ): + output_document.app = input_document.app + output_document.user = input_document.user + output_document.organization = input_document.organization + output_document.image = input_document.image + + @iterative_migration( + document_models=[OrganizationDB, UserDB, AppDB, AppEnvironmentDB] + ) + async def rename_app_environment_db_reference_to_link( + self, input_document: AppEnvironmentDB, output_document: AppEnvironmentDB + ): + output_document.app = input_document.app output_document.user = input_document.user + output_document.organization = input_document.organization + @iterative_migration(document_models=[OrganizationDB, UserDB, AppDB, TestSetDB]) + async def rename_testset_db_reference_to_link( + self, input_document: TestSetDB, output_document: TestSetDB + ): + output_document.app = input_document.app + output_document.user = input_document.user + output_document.organization = input_document.organization + + @iterative_migration( + document_models=[OrganizationDB, UserDB, AppDB, EvaluatorConfigDB] + ) + async def rename_evaluator_config_db_reference_to_link( + self, input_document: EvaluatorConfigDB, output_document: EvaluatorConfigDB + ): + output_document.app = input_document.app + output_document.user = input_document.user + output_document.organization = input_document.organization + + +class Backward: + pass From 109fbc80e5d33088bb8617b4acea50938aef4791 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Fri, 12 Jan 2024 17:51:01 +0500 Subject: [PATCH 185/267] row click improved | empty state ui in results page --- agenta-web/dev.Dockerfile | 60 +++---- .../evaluationResults/EvaluationResults.tsx | 155 +++++++++++------- .../pages/apps/[app_id]/evaluations/index.tsx | 2 +- agenta-web/src/services/evaluations/index.ts | 12 +- 4 files changed, 137 insertions(+), 92 deletions(-) diff --git a/agenta-web/dev.Dockerfile b/agenta-web/dev.Dockerfile index 6af573852c..ad5bc9a53b 100644 --- a/agenta-web/dev.Dockerfile +++ b/agenta-web/dev.Dockerfile @@ -1,37 +1,37 @@ FROM node:18-alpine -WORKDIR /app +# WORKDIR /app -# Install dependencies based on the preferred package manager -COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ -RUN \ - if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ - elif [ -f package-lock.json ]; then npm i; \ - elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ - # Allow install without lockfile, so example works even without Node.js installed locally - else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ - fi +# # Install dependencies based on the preferred package manager +# COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +# RUN \ +# if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ +# elif [ -f package-lock.json ]; then npm i; \ +# elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ +# # Allow install without lockfile, so example works even without Node.js installed locally +# else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ +# fi -COPY src ./src -COPY public ./public -COPY next.config.js . -COPY tsconfig.json . -COPY postcss.config.js . -COPY .env . -RUN if [ -f .env.local ]; then cp .env.local .; fi -# # used in cloud -COPY sentry.* . -# Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry -# Uncomment the following line to disable telemetry at run time -# ENV NEXT_TELEMETRY_DISABLED 1 +# COPY src ./src +# COPY public ./public +# COPY next.config.js . +# COPY tsconfig.json . +# COPY postcss.config.js . +# COPY .env . +# RUN if [ -f .env.local ]; then cp .env.local .; fi +# # # used in cloud +# COPY sentry.* . +# # Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry +# # Uncomment the following line to disable telemetry at run time +# # ENV NEXT_TELEMETRY_DISABLED 1 -# Note: Don't expose ports here, Compose will handle that for us +# # Note: Don't expose ports here, Compose will handle that for us -# Start Next.js in development mode based on the preferred package manager -CMD \ - if [ -f yarn.lock ]; then yarn dev; \ - elif [ -f package-lock.json ]; then npm run dev; \ - elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ - else yarn dev; \ - fi +# # Start Next.js in development mode based on the preferred package manager +# CMD \ +# if [ -f yarn.lock ]; then yarn dev; \ +# elif [ -f package-lock.json ]; then npm run dev; \ +# elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ +# else yarn dev; \ +# fi diff --git a/agenta-web/src/components/pages/evaluations/evaluationResults/EvaluationResults.tsx b/agenta-web/src/components/pages/evaluations/evaluationResults/EvaluationResults.tsx index 5758d86363..a1f9ea2590 100644 --- a/agenta-web/src/components/pages/evaluations/evaluationResults/EvaluationResults.tsx +++ b/agenta-web/src/components/pages/evaluations/evaluationResults/EvaluationResults.tsx @@ -3,7 +3,7 @@ import {AgGridReact} from "ag-grid-react" import {useAppTheme} from "@/components/Layout/ThemeContextProvider" import {ColDef} from "ag-grid-community" import {createUseStyles} from "react-jss" -import {Button, Space, Spin, Tag, Tooltip, theme} from "antd" +import {Button, Empty, Space, Spin, Tag, Tooltip, Typography, theme} from "antd" import {DeleteOutlined, PlusCircleOutlined, SlidersOutlined, SwapOutlined} from "@ant-design/icons" import {EvaluationStatus, GenericObject, JSSTheme, TypedValue, _Evaluation} from "@/lib/Types" import {capitalize, round, uniqBy} from "lodash" @@ -13,9 +13,8 @@ import duration from "dayjs/plugin/duration" import NewEvaluationModal from "./NewEvaluationModal" import {useAppId} from "@/hooks/useAppId" import {deleteEvaluations, fetchAllEvaluations, fetchEvaluationStatus} from "@/services/evaluations" -import {useRouter} from "next/router" import {useUpdateEffect} from "usehooks-ts" -import {redirectIfNoLLMKeys, shortPoll} from "@/lib/helpers/utils" +import {shortPoll} from "@/lib/helpers/utils" import AlertPopup from "@/components/AlertPopup/AlertPopup" import { LinkCellRenderer, @@ -26,10 +25,16 @@ import { import {useAtom} from "jotai" import {evaluatorsAtom} from "@/lib/atoms/evaluation" import AgCustomHeader from "@/components/AgCustomHeader/AgCustomHeader" +import {useRouter} from "next/router" dayjs.extend(relativeTime) dayjs.extend(duration) const useStyles = createUseStyles((theme: JSSTheme) => ({ + emptyRoot: { + height: "calc(100vh - 260px)", + display: "grid", + placeItems: "center", + }, root: { display: "flex", flexDirection: "column", @@ -100,13 +105,13 @@ const EvaluationResults: React.FC = () => { const {appTheme} = useAppTheme() const classes = useStyles() const appId = useAppId() - const router = useRouter() const [evaluations, setEvaluations] = useState<_Evaluation[]>([]) const [evaluators] = useAtom(evaluatorsAtom) const [newEvalModalOpen, setNewEvalModalOpen] = useState(false) const [fetching, setFetching] = useState(false) const [selected, setSelected] = useState<_Evaluation[]>([]) const stoppers = useRef() + const router = useRouter() const {token} = theme.useToken() const gridRef = useRef() @@ -314,58 +319,96 @@ const EvaluationResults: React.FC = () => { ) return ( -
- - - {compareDisabled ? ( - - {compareBtnNode} - - ) : ( - compareBtnNode - )} - - - -
- - ref={gridRef as any} - rowData={evaluations} - columnDefs={colDefs} - getRowId={(params) => params.data.id} - onRowDoubleClicked={(params) => - EvaluationStatus.FINISHED === params.data?.status && - router.push(`/apps/${appId}/evaluations/${params.data?.id}`) - } - rowSelection="multiple" - suppressRowClickSelection - onSelectionChanged={(event) => setSelected(event.api.getSelectedRows())} - tooltipShowDelay={0} - /> + <> + {!fetching && !evaluations.length ? ( +
+ + + + Or + + +
- - + ) : ( +
+ + + {compareDisabled ? ( + + {compareBtnNode} + + ) : ( + compareBtnNode + )} + + + +
+ + ref={gridRef as any} + rowData={evaluations} + columnDefs={colDefs} + getRowId={(params) => params.data.id} + onRowClicked={(params) => { + // ignore clicks on the checkbox col + if ( + params.eventPath?.find( + (item: any) => item.ariaColIndex === "1", + ) + ) + return + EvaluationStatus.FINISHED === params.data?.status && + router.push(`/apps/${appId}/evaluations/${params.data?.id}`) + }} + rowSelection="multiple" + suppressRowClickSelection + onSelectionChanged={(event) => + setSelected(event.api.getSelectedRows()) + } + tooltipShowDelay={0} + /> +
+
+
+ )} setNewEvalModalOpen(false)} @@ -374,7 +417,7 @@ const EvaluationResults: React.FC = () => { fetcher() }} /> -
+ ) } diff --git a/agenta-web/src/pages/apps/[app_id]/evaluations/index.tsx b/agenta-web/src/pages/apps/[app_id]/evaluations/index.tsx index 5abc7a7811..d4ff95e3fb 100644 --- a/agenta-web/src/pages/apps/[app_id]/evaluations/index.tsx +++ b/agenta-web/src/pages/apps/[app_id]/evaluations/index.tsx @@ -45,7 +45,7 @@ const Evaluations: React.FC = () => {
({ appId: item.app_id, created_at: item.created_at, updated_at: item.updated_at, - duration: dayjs( - [EvaluationStatus.STARTED, EvaluationStatus.INITIALIZED].includes(item.status) - ? Date.now() - : item.updated_at, - ).diff(dayjs(item.created_at), "milliseconds"), + duration: + 500000 || + dayjs( + [EvaluationStatus.STARTED, EvaluationStatus.INITIALIZED].includes(item.status) + ? Date.now() + : item.updated_at, + ).diff(dayjs(item.created_at), "milliseconds"), status: item.status, testset: { id: item.testset_id, From 60da49ff44bc3a6d1739879a8e32bdc2644a0de6 Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 12 Jan 2024 15:09:05 +0100 Subject: [PATCH 186/267] Update - set default version to old evaluation models --- .../migrations/20240110165900_evaluations_revamp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 9daadf3af2..6dd8e44e94 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -201,6 +201,7 @@ class OldEvaluationDB(Document): evaluation_type: str evaluation_type_settings: OldEvaluationTypeSettings variants: List[PydanticObjectId] + version: str = Field("odmantic") testset: Link[TestSetDB] created_at: Optional[datetime] = Field(default=datetime.utcnow()) updated_at: Optional[datetime] = Field(default=datetime.utcnow()) @@ -216,6 +217,7 @@ class OldEvaluationScenarioDB(Document): inputs: List[OldEvaluationScenarioInput] outputs: List[OldEvaluationScenarioOutput] # EvaluationScenarioOutput vote: Optional[str] + version: str = Field("odmantic") score: Optional[Any] correct_answer: Optional[str] created_at: Optional[datetime] = Field(default=datetime.utcnow()) @@ -230,6 +232,7 @@ class Settings: class OldCustomEvaluationDB(Document): evaluation_name: str python_code: str + version: str = Field("odmantic") app: Link[AppDB] user: Link[UserDB] organization: Link[OrganizationDB] From 08a89ac0792ab9e463dc786f3a58639fab4710a8 Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 12 Jan 2024 16:06:30 +0100 Subject: [PATCH 187/267] Update - added cleanup codes --- .../20240110165900_evaluations_revamp.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 6dd8e44e94..412f85419a 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -1,7 +1,8 @@ +import os from datetime import datetime from typing import Any, Dict, List, Optional - +from pymongo import MongoClient from pydantic import BaseModel, Field from beanie import free_fall_migration, Document, Link, PydanticObjectId @@ -265,6 +266,26 @@ def modify_app_id_store( app_id_store["evaluation_types"] = list(set(app_id_store_evaluation_types)) +def set_odmantic_version_in_old_evaluation_records(): + # Initialize mongo client + client = MongoClient("mongodb://username:password@192.168.16.1:27017") + db = client["agenta_v2"] + + evaluation_db = db.get_collection("evaluations") + evaluation_scenario_db = db.get_collection("evaluation_scenarios") + + def update_evaluation_version(): + for document in evaluation_db.find(): + document.update({"_id": document["_id"], "$set": {"version": "odmantic"}}) + + def update_evaluation_scenario_version(): + for document in evaluation_scenario_db.find(): + document.update({"_id": document["_id"], "$set": {"version": "odmantic"}}) + + update_evaluation_version() + update_evaluation_scenario_version() + + class Forward: @free_fall_migration( document_models=[ @@ -280,6 +301,11 @@ class Forward: ] ) async def migrate_old_evaluation_to_new_evaluation(self, session): + # PREPARATION: + # Update old evaluation, and scenario records version to + # odmantic to clean up records after use + set_odmantic_version_in_old_evaluation_records() + # STEP 1: # Create a key-value store that saves all the variants & evaluation types for a particular app id # Example: {"app_id": {"evaluation_types": ["string", "string"], "variant_ids": ["string", "string"]}} @@ -456,6 +482,11 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi ) await new_scenario.insert(session=session) + # # Cleanup: remove old evaluation records with odmantic as their version + await OldCustomEvaluationDB.find(lazy_parse=True).to_list() + await OldEvaluationDB.find({"version": "odmantic"}, lazy_parse=True).to_list() + await OldEvaluationScenarioDB.find({"version": "odmantic"}, lazy_parse=True).delete() + class Backward: pass From f3e07b537b2cd4b315c8bcdd1de1a3b50e41cab8 Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 12 Jan 2024 17:00:04 +0100 Subject: [PATCH 188/267] Update - renamed current evaluation, scenarios collection with 'new_' prefix --- .../20240110165900_evaluations_revamp.py | 33 ++----------------- .../agenta_backend/models/db_models.py | 4 +-- 2 files changed, 4 insertions(+), 33 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 412f85419a..d90add0e9a 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -152,7 +152,7 @@ class EvaluationDB(Document): updated_at: datetime = Field(default=datetime.utcnow()) class Settings: - name = "evaluations" + name = "new_evaluations" class EvaluationScenarioDB(Document): @@ -171,7 +171,7 @@ class EvaluationScenarioDB(Document): updated_at: datetime = Field(default=datetime.utcnow()) class Settings: - name = "evaluation_scenarios" + name = "new_evaluation_scenarios" class OldEvaluationTypeSettings(BaseModel): @@ -266,25 +266,6 @@ def modify_app_id_store( app_id_store["evaluation_types"] = list(set(app_id_store_evaluation_types)) -def set_odmantic_version_in_old_evaluation_records(): - # Initialize mongo client - client = MongoClient("mongodb://username:password@192.168.16.1:27017") - db = client["agenta_v2"] - - evaluation_db = db.get_collection("evaluations") - evaluation_scenario_db = db.get_collection("evaluation_scenarios") - - def update_evaluation_version(): - for document in evaluation_db.find(): - document.update({"_id": document["_id"], "$set": {"version": "odmantic"}}) - - def update_evaluation_scenario_version(): - for document in evaluation_scenario_db.find(): - document.update({"_id": document["_id"], "$set": {"version": "odmantic"}}) - - update_evaluation_version() - update_evaluation_scenario_version() - class Forward: @free_fall_migration( @@ -301,11 +282,6 @@ class Forward: ] ) async def migrate_old_evaluation_to_new_evaluation(self, session): - # PREPARATION: - # Update old evaluation, and scenario records version to - # odmantic to clean up records after use - set_odmantic_version_in_old_evaluation_records() - # STEP 1: # Create a key-value store that saves all the variants & evaluation types for a particular app id # Example: {"app_id": {"evaluation_types": ["string", "string"], "variant_ids": ["string", "string"]}} @@ -482,11 +458,6 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi ) await new_scenario.insert(session=session) - # # Cleanup: remove old evaluation records with odmantic as their version - await OldCustomEvaluationDB.find(lazy_parse=True).to_list() - await OldEvaluationDB.find({"version": "odmantic"}, lazy_parse=True).to_list() - await OldEvaluationScenarioDB.find({"version": "odmantic"}, lazy_parse=True).delete() - class Backward: pass diff --git a/agenta-backend/agenta_backend/models/db_models.py b/agenta-backend/agenta_backend/models/db_models.py index c336badf19..68d15450b8 100644 --- a/agenta-backend/agenta_backend/models/db_models.py +++ b/agenta-backend/agenta_backend/models/db_models.py @@ -290,7 +290,7 @@ class EvaluationDB(Document): updated_at: datetime = Field(default=datetime.utcnow()) class Settings: - name = "evaluations" + name = "new_evaluations" class EvaluationScenarioDB(Document): @@ -309,7 +309,7 @@ class EvaluationScenarioDB(Document): updated_at: datetime = Field(default=datetime.utcnow()) class Settings: - name = "evaluation_scenarios" + name = "new_evaluation_scenarios" class SpanDB(Document): From 4aedf0b70411126e339415636de70283d7b32208 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Fri, 12 Jan 2024 17:23:24 +0100 Subject: [PATCH 189/267] Delete unused templates and documentation files --- .../list_templates_by_architecture.mdx | 24 ------ .../cookbook/list_templates_by_technology.mdx | 19 ----- docs/cookbook/list_templates_by_use_case.mdx | 15 ---- .../contributing/api_reference.mdx | 0 .../contributing/community.mdx | 0 .../contributing/technical_details.mdx | 85 ------------------- .../contributing/terminology.mdx | 0 7 files changed, 143 deletions(-) delete mode 100644 docs/cookbook/list_templates_by_architecture.mdx delete mode 100644 docs/cookbook/list_templates_by_technology.mdx delete mode 100644 docs/cookbook/list_templates_by_use_case.mdx delete mode 100644 docs/developer_guides/contributing/api_reference.mdx delete mode 100644 docs/developer_guides/contributing/community.mdx delete mode 100644 docs/developer_guides/contributing/technical_details.mdx delete mode 100644 docs/developer_guides/contributing/terminology.mdx diff --git a/docs/cookbook/list_templates_by_architecture.mdx b/docs/cookbook/list_templates_by_architecture.mdx deleted file mode 100644 index 1adea1a975..0000000000 --- a/docs/cookbook/list_templates_by_architecture.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: "Templates by Architecture" -description: "A collection of templates and tutorials indexed by architecture." ---- - This page is a work in progress. Please note that some of the entries are redundant. - -# Tutorials -## 📝 Text Generation -### [Single Prompt Application using OpenAI and Langchain](/tutorials/first-app-with-langchain) -Learn how to use our SDK to deploy an application with agenta. The application we will create uses OpenAI and Langchain. The application generates outreach messages in Linkedin to investors based on a startup name and idea. -### [Use Mistral from Huggingface for a Summarization Task](/tutorials/deploy-mistral-model) -Learn how to use a custom model with agenta. - -## Retrieval Augmented Generation (RAG) -### [RAG Application with LlamaIndex](/tutorials/build-rag-application) -Learn how to create a RAG application with LlamaIndex and use it in agenta. You will create a playground in agenta where you can experiment with the parameters of the RAG application, test it and compare different versions. - -# Templates -## ⛏️ Extraction - -These templates extract data in a structured format from an unstructured source. - -### [Extraction using OpenAI Functions and Langchain](/cookbook/extract_job_information) -Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. diff --git a/docs/cookbook/list_templates_by_technology.mdx b/docs/cookbook/list_templates_by_technology.mdx deleted file mode 100644 index 1edeac6a53..0000000000 --- a/docs/cookbook/list_templates_by_technology.mdx +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: "Templates by Technology" -description: "A collection of templates and tutorials indexed by the used framework and model provider." ---- - - This page is a work in progress. Please note that some of the entries are redundant. - -## Langchain -### [Extraction using OpenAI Functions and Langchain](/cookbook/extract_job_information) -Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. - -## LlamaIndex -### [RAG Application with LlamaIndex](/developer_guides/tutorials/build-rag-application) -Learn how to create a RAG application with LlamaIndex and use it in agenta. You will create a playground in agenta where you can experiment with the parameters of the RAG application, test it and compare different versions. The application takes a sales transcript and answers questions based on it. - - -## OpenAI -### [Extraction using OpenAI Functions and Langchain](/cookbook/extract_job_information) -Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. diff --git a/docs/cookbook/list_templates_by_use_case.mdx b/docs/cookbook/list_templates_by_use_case.mdx deleted file mode 100644 index 67f5c77f38..0000000000 --- a/docs/cookbook/list_templates_by_use_case.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Templates by Use Case" -description: "A collection of templates and tutorials indexed by the the use case." ---- - This page is a work in progress. Please note that some of the entries are redundant. - -## Human Ressources -### [Extraction using OpenAI Functions and Langchain](/cookbook/extract_job_information) -Extracts job information (company name, job title, salary range) from a job description. Uses OpenAI Functions and Langchain. - -## Sales -### [Single Prompt Application using OpenAI and Langchain](/developer_guides/tutorials/first-app-with-langchain) -Learn how to use our SDK to deploy an application with agenta. The application we will create uses OpenAI and Langchain. The application generates outreach messages in Linkedin to investors based on a startup name and idea. -### [RAG Application with LlamaIndex](/developer_guides/tutorials/build-rag-application) -Learn how to create a RAG application with LlamaIndex and use it in agenta. You will create a playground in agenta where you can experiment with the parameters of the RAG application, test it and compare different versions. The application takes a sales transcript and answers questions based on it. diff --git a/docs/developer_guides/contributing/api_reference.mdx b/docs/developer_guides/contributing/api_reference.mdx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/developer_guides/contributing/community.mdx b/docs/developer_guides/contributing/community.mdx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/developer_guides/contributing/technical_details.mdx b/docs/developer_guides/contributing/technical_details.mdx deleted file mode 100644 index 7cb108b408..0000000000 --- a/docs/developer_guides/contributing/technical_details.mdx +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: "Diving Deep into Agenta's Components" -description: "A comprehensive technical overview of Agenta's architecture" ---- - - -# In-depth on CLI - -## Commands - -### `agenta variant serve` - -When you execute `agenta variant serve`, it performs the following tasks: - -Copies your code into a temporary folder and supplements it with our proprietary code (dockerfile, agenta.py, etc.) -Constructs a Docker image from the content in the temporary folder -Uploads the Docker image to the registry -Notifies the API about the new Docker image -Instructs the backend to launch the Docker image - -# In-depth on backend - -## Creating a New App -Users can create a new app in two ways: via the CLI or the web app. - -### CLI -Upon executing `agenta init`, we solicit the app name and store it in `config.toml`. At this stage, the app exists only locally within the user's directory. It gets incorporated into the backend only after the user executes the `agenta variant serve` command. - -When a user runs `agenta variant serve` for the first time within an app, we instantiate the new app in the backend. For each newly served variant, we add a new variant to the app name specified in `config.toml`. - - - -### Web app -Users can also create a new app via the web app. However, new apps in this context can only be created based on a template. This implies that the user won't have access to the app code but can only use the container. If the user requires access to the app code, they must initiate the process from the CLI. - -When a user starts a new app from the web app, they need to select a template. In this case, we instruct the backend to initiate an app from a template. The template is a Docker image located in some registry which the backend downloads, adds to the registry, initializes a new app, and subsequently launches it. - - -## Creating a new variant - -### CLI -The procedure is exactly the same as when creating a new app, even down to the API calls. The only difference is that the backend adds a new variant to an existing app rather than creating a new app (according to the database schema). - - -### Web app -Users can also create a new app variant by forking an existing one via the web UI. -When a user formulates a new variant in the playground, it only virtually exists, with its parameters stored in the UI. The variant truly comes into existence only when the user saves the new variant. At this stage, the backend replicates the new variant and mounts a new volume to the image with the new default parameters. -In this scenario, the SDK bypasses the code's default parameters and instead uses those within the mounted volume. - -# Database Schema - -Our current schema is rudementary: - -```python -class ImageDB(SQLModel, table=True): - """Defines the info needed to get an image and connect it to the app variant - """ - id: int = Field(default=None, primary_key=True) - docker_id: str = Field(...) - tags: str = Field(...) - - -class AppVariantDB(SQLModel, table=True): - """Defines an app variant and connects to an image """ - id: Optional[int] = Field(default=None, primary_key=True) - app_name: str = Field(...) - variant_name: str = Field(...) - image_id: int = Field(foreign_key="imagedb.id") - - -class App(SQLModel, table=True): - """Defines an app """ - id: int = Field(default=None, primary_key=True) - app_name: str = Field(...) -``` - -The first time the user adds an app variant, we update the App table then the AppVariant table. Subsequently, we only update the AppVariant table. - - -# Future Considerations: -An alternative approach to implementing this is by modifying the image code to incorporate new elements as files. However, this would imply that the backend has access to the code. - -Looking forward, if Agenta is linked to git, many processes could be simplified. - -If the user initiates a variant from the web UI, we could facilitate them to fork a GitHub repository into their own account. Subsequently, we could create a new app in the backend (transferring the CLI's logic to the backend), allowing us to access the code. We could then generate new backend variants by modifying the code in the repository. The main concern here is speed. Creating new variants would involve pulling the code, altering it, creating a new branch, pushing, building the image, and starting it, which is time-consuming. However, this process could potentially be done in the background. The primary consideration remains the importance of versioning. diff --git a/docs/developer_guides/contributing/terminology.mdx b/docs/developer_guides/contributing/terminology.mdx deleted file mode 100644 index e69de29bb2..0000000000 From 17023e159bf38962725f791877d48f8c5c38460c Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Fri, 12 Jan 2024 17:23:35 +0100 Subject: [PATCH 190/267] changelog update --- docs/changelog/main.mdx | 27 ++++++++++++++++++ .../screenshot_cost_and_token_usage.png | Bin 0 -> 452718 bytes 2 files changed, 27 insertions(+) create mode 100644 docs/images/changelog/screenshot_cost_and_token_usage.png diff --git a/docs/changelog/main.mdx b/docs/changelog/main.mdx index f0da6c86a4..f7dc41850c 100644 --- a/docs/changelog/main.mdx +++ b/docs/changelog/main.mdx @@ -2,6 +2,33 @@ title: "Changelog" --- +## v0.7.1 - Adding Cost and Token Usage to the Playground + +*12th January 2023* + This change requires you to pull the latest version of the agenta platform if you're using the self-serve version. + + + +We've added a feature that allows you to compare the time taken by an LLM app, its cost, and track token usage, all in one place. + +### Changes to the SDK + +This necessitated modifications to the SDK. Now, the LLM application API returns a JSON instead of a string. The JSON includes the output message, usage details, and cost: + +``` +{ + "message": string, + "usage": { + "prompt_tokens": int, + "completion_tokens": int, + "total_tokens": int + }, + "cost": float +} +``` + + + ## v0.6.6 - Improving Side-by-side Comparison in the Playground *19th December 2023* - Enhanced the side-by-side comparison in the playground for better user experience diff --git a/docs/images/changelog/screenshot_cost_and_token_usage.png b/docs/images/changelog/screenshot_cost_and_token_usage.png new file mode 100644 index 0000000000000000000000000000000000000000..66482e86f009a71e999ce7bd3e525a6541349727 GIT binary patch literal 452718 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�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 iRLJk?nK@-o|1)-PpRqSu;#DXE5O})!xvX Date: Fri, 12 Jan 2024 17:31:45 +0100 Subject: [PATCH 191/267] Update - modified migration for evaluations revamp --- .../20240110165900_evaluations_revamp.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index d90add0e9a..0dbf78ac43 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -310,27 +310,27 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): for custom_code_evaluation in custom_code_evaluations: eval_config = EvaluatorConfigDB( app=PydanticObjectId(app_id), - organization=old_eval.organization.id, - user=old_eval.user.id, + organization=old_eval.organization, + user=old_eval.user, name=f"{old_eval.app.app_name}_{old_eval.evaluation_type}", evaluator_key=f"auto_{evaluation_type}", settings_values={} if custom_code_evaluation is None else {"code": custom_code_evaluation.python_code}, ) - await eval_config.create(session=session) + await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) if evaluation_type != "custom_code_run": eval_config = EvaluatorConfigDB( app=PydanticObjectId(app_id), - organization=old_eval.organization.id, - user=old_eval.user.id, + organization=old_eval.organization, + user=old_eval.user, name=f"{old_eval.app.app_name}_{old_eval.evaluation_type}", evaluator_key=evaluation_type, settings_values={}, ) - await eval_config.create(session=session) + await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) # STEP 3 (a): @@ -343,7 +343,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): "human_a_b_testing", "single_model_test", ]: - auto_evaluator_configs.append(evaluator_config.id) + auto_evaluator_configs.append(PydanticObjectId(evaluator_config.id)) # STEP 3 (b): # In the case where the evaluator key is a human evaluator, @@ -355,14 +355,14 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): ]: new_eval = HumanEvaluationDB( app=PydanticObjectId(app_id), - organization=old_eval.organization.id, - user=old_eval.user.id, + organization=old_eval.organization, + user=old_eval.user, status=old_eval.status, evaluation_type=evaluator_config.evaluator_key, variants=app_id_store["variant_ids"], - testset=old_eval.testset.id, + testset=old_eval.testset, ) - await new_eval.create(session=session) # replace(session=session) + await new_eval.insert(session=session) # replace(session=session) # STEP 3 (c): # Proceed to create a single evaluation for every variant in the app_id_store @@ -371,15 +371,15 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): for variant in app_id_store["variant_ids"]: new_eval = EvaluationDB( app=PydanticObjectId(app_id), - organization=old_eval.organization.id, - user=old_eval.user.id, + organization=old_eval.organization, + user=old_eval.user, status=old_eval.status, - testset=old_eval.testset.id, - variant=variant, + testset=old_eval.testset, + variant=PydanticObjectId(variant), evaluators_configs=auto_evaluator_configs, aggregated_results=[], ) - await new_eval.create(session=session) + await new_eval.insert(session=session) @free_fall_migration( document_models=[ @@ -417,9 +417,9 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi for output in old_scenario.outputs ] new_scenario = HumanEvaluationScenarioDB( - user=old_scenario.user.id, - organization=old_scenario.organization.id, - evaluation=old_scenario.evaluation.id, + user=old_scenario.user, + organization=old_scenario.organization, + evaluation=old_scenario.evaluation, inputs=scenario_inputs, outputs=scenario_outputs, correct_answer=old_scenario.correct_answer, @@ -431,9 +431,9 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi await new_scenario.insert(session=session) else: new_scenario = EvaluationScenarioDB( - user=old_scenario.user.id, - organization=old_scenario.organization.id, - evaluation=old_scenario.evaluation.id, + user=old_scenario.user, + organization=old_scenario.organization, + evaluation=old_scenario.evaluation, variant_id=old_scenario.evaluation.variants[0], inputs=[ EvaluationScenarioInputDB( From 3e8cbce89530bbc1a5eb470a6ba4ce32ed0eb2df Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 12 Jan 2024 18:59:30 +0100 Subject: [PATCH 192/267] Update - modified logic to migrate old evaluation scenario --- .../20240110165900_evaluations_revamp.py | 125 ++++++++++-------- 1 file changed, 69 insertions(+), 56 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 0dbf78ac43..eb7feed807 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -266,7 +266,6 @@ def modify_app_id_store( app_id_store["evaluation_types"] = list(set(app_id_store_evaluation_types)) - class Forward: @free_fall_migration( document_models=[ @@ -397,66 +396,80 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): ) async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, session): old_scenarios = await OldEvaluationScenarioDB.find(fetch_links=True).to_list() + new_evaluations = await EvaluationDB.find(fetch_links=True).to_list() + new_human_evaluations = await HumanEvaluationDB.find(fetch_links=True).to_list() + combined_evaluations = new_evaluations + new_human_evaluations for old_scenario in old_scenarios: - if old_scenario.evaluation.evaluation_type in [ - "human_a_b_testing", - "single_model_test", - ]: - scenario_inputs = [ - HumanEvaluationScenarioInput( - input_name=input.input_name, - input_value=input.input_value, - ) - for input in old_scenario.inputs - ] - scenario_outputs = [ - HumanEvaluationScenarioOutput( - variant_id=output.variant_id, - variant_output=output.variant_output, - ) - for output in old_scenario.outputs - ] - new_scenario = HumanEvaluationScenarioDB( - user=old_scenario.user, - organization=old_scenario.organization, - evaluation=old_scenario.evaluation, - inputs=scenario_inputs, - outputs=scenario_outputs, - correct_answer=old_scenario.correct_answer, - is_pinned=old_scenario.is_pinned, - note=old_scenario.note, - vote=old_scenario.vote, - score=old_scenario.score, - ) - await new_scenario.insert(session=session) - else: - new_scenario = EvaluationScenarioDB( - user=old_scenario.user, - organization=old_scenario.organization, - evaluation=old_scenario.evaluation, - variant_id=old_scenario.evaluation.variants[0], - inputs=[ - EvaluationScenarioInputDB( - name=input.input_name, - type=type(input.input_value).__name__, - value=input.input_value, + for new_evaluation in combined_evaluations: + if type( + new_evaluation + ) == HumanEvaluationDB and old_scenario.evaluation.evaluation_type in [ + "human_a_b_testing", + "single_model_test", + ]: + scenario_inputs = [ + HumanEvaluationScenarioInput( + input_name=input.input_name, + input_value=input.input_value, ) for input in old_scenario.inputs - ], - outputs=[ - EvaluationScenarioOutputDB( - type=type(output.variant_output).__name__, - value=output.variant_output, + ] + scenario_outputs = [ + HumanEvaluationScenarioOutput( + variant_id=output.variant_id, + variant_output=output.variant_output, ) for output in old_scenario.outputs - ], - correct_answer=old_scenario.correct_answer, - is_pinned=old_scenario.is_pinned, - note=old_scenario.note, - evaluators_configs=[], - results=[], - ) - await new_scenario.insert(session=session) + ] + if old_scenario.evaluation.app.id == new_evaluation.app.id: + new_scenario = HumanEvaluationScenarioDB( + user=new_evaluation.user, + organization=new_evaluation.organization, + evaluation=new_evaluation, + inputs=scenario_inputs, + outputs=scenario_outputs, + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + vote=old_scenario.vote, + score=old_scenario.score, + ) + await new_scenario.insert(session=session) + + if type( + new_evaluation + ) == EvaluationDB and old_scenario.evaluation.evaluation_type not in [ + "human_a_b_testing", + "single_model_test", + ]: + if old_scenario.evaluation.app.id == new_evaluation.app.id: + new_scenario = EvaluationScenarioDB( + user=new_evaluation.user, + organization=new_evaluation.organization, + evaluation=new_evaluation, + variant_id=old_scenario.evaluation.variants[0], + inputs=[ + EvaluationScenarioInputDB( + name=input.input_name, + type=type(input.input_value).__name__, + value=input.input_value, + ) + for input in old_scenario.inputs + ], + outputs=[ + EvaluationScenarioOutputDB( + type=type(output.variant_output).__name__, + value=output.variant_output, + ) + for output in old_scenario.outputs + ], + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + evaluators_configs=[], + results=[], + ) + await new_scenario.insert(session=session) class Backward: From 9f8f79836a91de7a59a8fd5ff2bb7ada69159620 Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 12 Jan 2024 19:23:45 +0100 Subject: [PATCH 193/267] Update - refactor migrate old evaluation scenario logic --- .../20240110165900_evaluations_revamp.py | 129 ++++++++---------- 1 file changed, 59 insertions(+), 70 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index eb7feed807..5fc389d70a 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -398,78 +398,67 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi old_scenarios = await OldEvaluationScenarioDB.find(fetch_links=True).to_list() new_evaluations = await EvaluationDB.find(fetch_links=True).to_list() new_human_evaluations = await HumanEvaluationDB.find(fetch_links=True).to_list() - combined_evaluations = new_evaluations + new_human_evaluations + for old_scenario in old_scenarios: - for new_evaluation in combined_evaluations: - if type( - new_evaluation - ) == HumanEvaluationDB and old_scenario.evaluation.evaluation_type in [ - "human_a_b_testing", - "single_model_test", - ]: - scenario_inputs = [ - HumanEvaluationScenarioInput( - input_name=input.input_name, - input_value=input.input_value, - ) - for input in old_scenario.inputs - ] - scenario_outputs = [ - HumanEvaluationScenarioOutput( - variant_id=output.variant_id, - variant_output=output.variant_output, - ) - for output in old_scenario.outputs - ] - if old_scenario.evaluation.app.id == new_evaluation.app.id: - new_scenario = HumanEvaluationScenarioDB( - user=new_evaluation.user, - organization=new_evaluation.organization, - evaluation=new_evaluation, - inputs=scenario_inputs, - outputs=scenario_outputs, - correct_answer=old_scenario.correct_answer, - is_pinned=old_scenario.is_pinned, - note=old_scenario.note, - vote=old_scenario.vote, - score=old_scenario.score, - ) - await new_scenario.insert(session=session) + for evaluation in new_evaluations: + if old_scenario.evaluation.app.id == evaluation.app.id: + new_scenario = EvaluationScenarioDB( + user=evaluation.user, + organization=evaluation.organization, + evaluation=evaluation, + variant_id=old_scenario.evaluation.variants[0], + inputs=[ + EvaluationScenarioInputDB( + name=input.input_name, + type=type(input.input_value).__name__, + value=input.input_value, + ) + for input in old_scenario.inputs + ], + outputs=[ + EvaluationScenarioOutputDB( + type=type(output.variant_output).__name__, + value=output.variant_output, + ) + for output in old_scenario.outputs + ], + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + evaluators_configs=[], + results=[], + ) + await new_scenario.insert(session=session) - if type( - new_evaluation - ) == EvaluationDB and old_scenario.evaluation.evaluation_type not in [ - "human_a_b_testing", - "single_model_test", - ]: - if old_scenario.evaluation.app.id == new_evaluation.app.id: - new_scenario = EvaluationScenarioDB( - user=new_evaluation.user, - organization=new_evaluation.organization, - evaluation=new_evaluation, - variant_id=old_scenario.evaluation.variants[0], - inputs=[ - EvaluationScenarioInputDB( - name=input.input_name, - type=type(input.input_value).__name__, - value=input.input_value, - ) - for input in old_scenario.inputs - ], - outputs=[ - EvaluationScenarioOutputDB( - type=type(output.variant_output).__name__, - value=output.variant_output, - ) - for output in old_scenario.outputs - ], - correct_answer=old_scenario.correct_answer, - is_pinned=old_scenario.is_pinned, - note=old_scenario.note, - evaluators_configs=[], - results=[], - ) - await new_scenario.insert(session=session) + for evaluation in new_human_evaluations: + scenario_inputs = [ + HumanEvaluationScenarioInput( + input_name=input.input_name, + input_value=input.input_value, + ) + for input in old_scenario.inputs + ] + scenario_outputs = [ + HumanEvaluationScenarioOutput( + variant_id=output.variant_id, + variant_output=output.variant_output, + ) + for output in old_scenario.outputs + ] + if old_scenario.evaluation.app.id == evaluation.app.id: + new_scenario = HumanEvaluationScenarioDB( + user=evaluation.user, + organization=evaluation.organization, + evaluation=evaluation, + inputs=scenario_inputs, + outputs=scenario_outputs, + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + vote=old_scenario.vote, + score=old_scenario.score, + ) + await new_scenario.insert(session=session) class Backward: From 784adea3ab55f03af04c983718333225f76788fe Mon Sep 17 00:00:00 2001 From: Abram Date: Fri, 12 Jan 2024 21:09:25 +0100 Subject: [PATCH 194/267] Update - added print debug statements --- .../migrations/20240110165900_evaluations_revamp.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 5fc389d70a..69c552f622 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -308,10 +308,10 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): if evaluation_type == "custom_code_run": for custom_code_evaluation in custom_code_evaluations: eval_config = EvaluatorConfigDB( - app=PydanticObjectId(app_id), + app=old_eval.app, organization=old_eval.organization, user=old_eval.user, - name=f"{old_eval.app.app_name}_{old_eval.evaluation_type}", + name=f"{old_eval.app.app_name}_{evaluation_type}", evaluator_key=f"auto_{evaluation_type}", settings_values={} if custom_code_evaluation is None @@ -322,10 +322,10 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): if evaluation_type != "custom_code_run": eval_config = EvaluatorConfigDB( - app=PydanticObjectId(app_id), + app=old_eval.app, organization=old_eval.organization, user=old_eval.user, - name=f"{old_eval.app.app_name}_{old_eval.evaluation_type}", + name=f"{old_eval.app.app_name}_{evaluation_type}", evaluator_key=evaluation_type, settings_values={}, ) @@ -401,6 +401,7 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi for old_scenario in old_scenarios: for evaluation in new_evaluations: + print(f"Checking scenario for evaluation: {old_scenario.evaluation.app.id} == {evaluation.app.id}") if old_scenario.evaluation.app.id == evaluation.app.id: new_scenario = EvaluationScenarioDB( user=evaluation.user, @@ -431,6 +432,7 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi await new_scenario.insert(session=session) for evaluation in new_human_evaluations: + print(f"Checking human scenario for evaluation: {old_scenario.evaluation.app.id} == {evaluation.app.id}") scenario_inputs = [ HumanEvaluationScenarioInput( input_name=input.input_name, From 5afb7b9bf35b7fd6c05cadb19d678cd9a35b6dd7 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 00:28:04 +0100 Subject: [PATCH 195/267] Update - modified migrate old evaluation scenario logic --- .../20240110165900_evaluations_revamp.py | 98 ++++++++++--------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 69c552f622..d1177acfd5 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -400,39 +400,44 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi new_human_evaluations = await HumanEvaluationDB.find(fetch_links=True).to_list() for old_scenario in old_scenarios: - for evaluation in new_evaluations: - print(f"Checking scenario for evaluation: {old_scenario.evaluation.app.id} == {evaluation.app.id}") - if old_scenario.evaluation.app.id == evaluation.app.id: - new_scenario = EvaluationScenarioDB( - user=evaluation.user, - organization=evaluation.organization, - evaluation=evaluation, - variant_id=old_scenario.evaluation.variants[0], - inputs=[ - EvaluationScenarioInputDB( - name=input.input_name, - type=type(input.input_value).__name__, - value=input.input_value, - ) - for input in old_scenario.inputs - ], - outputs=[ - EvaluationScenarioOutputDB( - type=type(output.variant_output).__name__, - value=output.variant_output, - ) - for output in old_scenario.outputs - ], - correct_answer=old_scenario.correct_answer, - is_pinned=old_scenario.is_pinned, - note=old_scenario.note, - evaluators_configs=[], - results=[], - ) - await new_scenario.insert(session=session) - - for evaluation in new_human_evaluations: - print(f"Checking human scenario for evaluation: {old_scenario.evaluation.app.id} == {evaluation.app.id}") + matching_evaluations = [ + evaluation for evaluation in new_evaluations if old_scenario.evaluation.app.id == evaluation.app.id + ] + for evaluation in matching_evaluations: + new_scenario = EvaluationScenarioDB( + user=evaluation.user, + organization=evaluation.organization, + evaluation=evaluation, + variant_id=old_scenario.evaluation.variants[0], + inputs=[ + EvaluationScenarioInputDB( + name=input.input_name, + type=type(input.input_value).__name__, + value=input.input_value, + ) + for input in old_scenario.inputs + ], + outputs=[ + EvaluationScenarioOutputDB( + type=type(output.variant_output).__name__, + value=output.variant_output, + ) + for output in old_scenario.outputs + ], + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + evaluators_configs=[], + results=[], + ) + await new_scenario.insert(session=session) + + matching_human_evaluations = [ + evaluation + for evaluation in new_human_evaluations + if old_scenario.evaluation.app.id == evaluation.app.id + ] + for human_evaluation in matching_human_evaluations: scenario_inputs = [ HumanEvaluationScenarioInput( input_name=input.input_name, @@ -447,20 +452,19 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi ) for output in old_scenario.outputs ] - if old_scenario.evaluation.app.id == evaluation.app.id: - new_scenario = HumanEvaluationScenarioDB( - user=evaluation.user, - organization=evaluation.organization, - evaluation=evaluation, - inputs=scenario_inputs, - outputs=scenario_outputs, - correct_answer=old_scenario.correct_answer, - is_pinned=old_scenario.is_pinned, - note=old_scenario.note, - vote=old_scenario.vote, - score=old_scenario.score, - ) - await new_scenario.insert(session=session) + new_scenario = HumanEvaluationScenarioDB( + user=human_evaluation.user, + organization=human_evaluation.organization, + evaluation=human_evaluation, + inputs=scenario_inputs, + outputs=scenario_outputs, + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + vote=old_scenario.vote, + score=old_scenario.score, + ) + await new_scenario.insert(session=session) class Backward: From 7b3d6649df898dcb01de097611f2db5c8d68d691 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 10:39:39 +0100 Subject: [PATCH 196/267] Update - cleanup evaluation service and modified evaluations revamp migration logic --- .../20240110165900_evaluations_revamp.py | 34 +++++++++++++++---- .../services/evaluation_service.py | 4 --- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index d1177acfd5..95e60586f2 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -320,7 +320,19 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) - if evaluation_type != "custom_code_run": + if evaluation_type == "auto_similarity_match": + eval_config = EvaluatorConfigDB( + app=old_eval.app, + organization=old_eval.organization, + user=old_eval.user, + name=f"{old_eval.app.app_name}_{evaluation_type}", + evaluator_key=evaluation_type, + settings_values={"similarity_threshold": 0.5}, + ) + await eval_config.insert(session=session) + app_evaluator_configs.append(eval_config) + + if evaluation_type not in ["custom_code_run", "auto_similarity_match"]: eval_config = EvaluatorConfigDB( app=old_eval.app, organization=old_eval.organization, @@ -361,7 +373,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): variants=app_id_store["variant_ids"], testset=old_eval.testset, ) - await new_eval.insert(session=session) # replace(session=session) + await new_eval.insert(session=session) # STEP 3 (c): # Proceed to create a single evaluation for every variant in the app_id_store @@ -376,7 +388,6 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): testset=old_eval.testset, variant=PydanticObjectId(variant), evaluators_configs=auto_evaluator_configs, - aggregated_results=[], ) await new_eval.insert(session=session) @@ -401,9 +412,18 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi for old_scenario in old_scenarios: matching_evaluations = [ - evaluation for evaluation in new_evaluations if old_scenario.evaluation.app.id == evaluation.app.id + evaluation + for evaluation in new_evaluations + if old_scenario.evaluation.app == evaluation.app.id ] for evaluation in matching_evaluations: + results = [ + EvaluationScenarioResult( + evaluator_config=PydanticObjectId(evaluator_config), + result=old_scenario.score, + ) + for evaluator_config in evaluation.evaluators_configs + ] new_scenario = EvaluationScenarioDB( user=evaluation.user, organization=evaluation.organization, @@ -427,15 +447,15 @@ async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, sessi correct_answer=old_scenario.correct_answer, is_pinned=old_scenario.is_pinned, note=old_scenario.note, - evaluators_configs=[], - results=[], + evaluators_configs=evaluation.evaluators_configs, + results=results, ) await new_scenario.insert(session=session) matching_human_evaluations = [ evaluation for evaluation in new_human_evaluations - if old_scenario.evaluation.app.id == evaluation.app.id + if old_scenario.evaluation.app == evaluation.app.id ] for human_evaluation in matching_human_evaluations: scenario_inputs = [ diff --git a/agenta-backend/agenta_backend/services/evaluation_service.py b/agenta-backend/agenta_backend/services/evaluation_service.py index 3182b8d393..db79d2f55f 100644 --- a/agenta-backend/agenta_backend/services/evaluation_service.py +++ b/agenta-backend/agenta_backend/services/evaluation_service.py @@ -610,9 +610,6 @@ async def create_new_human_evaluation( """ user = await get_user(user_uid=user_org_data["uid"]) - # Initialize evaluation type settings - evaluation_type_settings = {} - current_time = datetime.utcnow() # Fetch app @@ -633,7 +630,6 @@ async def create_new_human_evaluation( user=user, status=payload.status, evaluation_type=payload.evaluation_type, - evaluation_type_settings=evaluation_type_settings, variants=variants, testset=testset, created_at=current_time, From 8dd75b3026c1a692c237f2d20166857ee330d293 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Sat, 13 Jan 2024 15:17:56 +0100 Subject: [PATCH 197/267] improved no variants testcase --- agenta-web/src/pages/apps/[app_id]/endpoints/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agenta-web/src/pages/apps/[app_id]/endpoints/index.tsx b/agenta-web/src/pages/apps/[app_id]/endpoints/index.tsx index 65816b4e43..29615ed402 100644 --- a/agenta-web/src/pages/apps/[app_id]/endpoints/index.tsx +++ b/agenta-web/src/pages/apps/[app_id]/endpoints/index.tsx @@ -7,7 +7,7 @@ import {Environment, GenericObject, Parameter, Variant} from "@/lib/Types" import {useVariant} from "@/lib/hooks/useVariant" import {fetchEnvironments, fetchVariants, getAppContainerURL} from "@/lib/services/api" import {ApiOutlined, DownOutlined} from "@ant-design/icons" -import {Alert, Button, Dropdown, Space, Typography} from "antd" +import {Alert, Button, Dropdown, Empty, Space, Typography} from "antd" import {useRouter} from "next/router" import {useEffect, useState} from "react" import {createUseStyles} from "react-jss" @@ -136,7 +136,7 @@ export default function VariantEndpoint() { return } if (!variant) { - return + return } if (isLoading) { return From 74fd7cf7f7241650c7f15b1b80c13b829ad8568e Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Sat, 13 Jan 2024 15:50:20 +0100 Subject: [PATCH 198/267] fix: cancel logic to cancel request of clicked row --- .../components/Playground/Views/TestView.tsx | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/agenta-web/src/components/Playground/Views/TestView.tsx b/agenta-web/src/components/Playground/Views/TestView.tsx index e81618ba74..2fd02bc0e9 100644 --- a/agenta-web/src/components/Playground/Views/TestView.tsx +++ b/agenta-web/src/components/Playground/Views/TestView.tsx @@ -216,6 +216,7 @@ const BoxComponent: React.FC = ({ type="primary" style={{backgroundColor: "#d32f2f"}} onClick={onCancel} + className={`testview-cancel-button-${testData._id}`} > Cancel @@ -410,6 +411,37 @@ const App: React.FC = ({ } } + const handleCancel = (index: number) => { + if (abortControllersRef.current[index]) { + abortControllersRef.current[index].abort() + } + + const testItem = testList[index] + setIsRunning( + (prevState) => { + const newState = [...prevState] + newState[index] = false + return newState + }, + () => { + document + .querySelectorAll(`.testview-cancel-button-${testItem._id}`) + .forEach((btn) => { + if (btn.parentElement?.id !== variant.variantId) { + ;(btn as HTMLButtonElement).click() + } + }) + }, + ) + + setResultForIndex("", index) + setAdditionalDataList((prev) => { + const newDataList = [...prev] + newDataList[index] = {cost: null, latency: null, usage: null} + return newDataList + }) + } + const handleCancelAll = () => { abortControllersRef.current.forEach((controller, index) => { if (controller) { @@ -498,25 +530,6 @@ const App: React.FC = ({ } } - const handleCancel = (index: number) => { - if (abortControllersRef.current[index]) { - abortControllersRef.current[index].abort() - } - - setIsRunning((prevState) => { - const newState = [...prevState] - newState[index] = false - return newState - }) - - setResultForIndex("", index) - setAdditionalDataList((prev) => { - const newDataList = [...prev] - newDataList[index] = {cost: null, latency: null, usage: null} - return newDataList - }) - } - return (

From 01a32e389b0616fab08de7cfd8039c18d2a5cad1 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 15:56:04 +0100 Subject: [PATCH 199/267] Feat - created evaluation scenarios revamp migration --- ...40113131802_evaluation_scenarios_revamp.py | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 agenta-backend/agenta_backend/migrations/20240113131802_evaluation_scenarios_revamp.py diff --git a/agenta-backend/agenta_backend/migrations/20240113131802_evaluation_scenarios_revamp.py b/agenta-backend/agenta_backend/migrations/20240113131802_evaluation_scenarios_revamp.py new file mode 100644 index 0000000000..d8ba377e79 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/20240113131802_evaluation_scenarios_revamp.py @@ -0,0 +1,412 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + +from beanie.operators import In +from pydantic import BaseModel, Field +from beanie import free_fall_migration, Document, Link, PydanticObjectId + + +class OrganizationDB(Document): + name: str = Field(default="agenta") + description: str = Field(default="") + type: Optional[str] + owner: str # user id + members: Optional[List[PydanticObjectId]] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "organizations" + + +class UserDB(Document): + uid: str = Field(default="0", unique=True, index=True) + username: str = Field(default="agenta") + email: str = Field(default="demo@agenta.ai", unique=True) + organizations: Optional[List[PydanticObjectId]] = [] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "users" + + +class AppDB(Document): + app_name: str + organization: Link[OrganizationDB] + user: Link[UserDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "app_db" + + +class TestSetDB(Document): + name: str + app: Link[AppDB] + csvdata: List[Dict[str, str]] + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "testsets" + + +class EvaluatorConfigDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + name: str + evaluator_key: str + settings_values: Optional[Dict[str, Any]] = None + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluators_configs" + + +class Result(BaseModel): + type: str + value: Any + + +class EvaluationScenarioResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class AggregatedResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class EvaluationScenarioInputDB(BaseModel): + name: str + type: str + value: str + + +class EvaluationScenarioOutputDB(BaseModel): + type: str + value: Any + + +class HumanEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class HumanEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class HumanEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + variants: List[PydanticObjectId] + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "human_evaluations" + + +class HumanEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[HumanEvaluationDB] + inputs: List[HumanEvaluationScenarioInput] + outputs: List[HumanEvaluationScenarioOutput] + vote: Optional[str] + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "human_evaluations_scenarios" + + +class EvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str = Field(default="EVALUATION_INITIALIZED") + testset: Link[TestSetDB] + variant: PydanticObjectId + evaluators_configs: List[PydanticObjectId] + aggregated_results: List[AggregatedResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "new_evaluations" + + +class EvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[EvaluationDB] + variant_id: PydanticObjectId + inputs: List[EvaluationScenarioInputDB] + outputs: List[EvaluationScenarioOutputDB] + correct_answer: Optional[str] + is_pinned: Optional[bool] + note: Optional[str] + evaluators_configs: List[PydanticObjectId] + results: List[EvaluationScenarioResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "new_evaluation_scenarios" + + +class OldEvaluationTypeSettings(BaseModel): + similarity_threshold: Optional[float] + regex_pattern: Optional[str] + regex_should_match: Optional[bool] + webhook_url: Optional[str] + llm_app_prompt_template: Optional[str] + custom_code_evaluation_id: Optional[str] + evaluation_prompt_template: Optional[str] + + +class OldEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class OldEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class OldEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + evaluation_type_settings: OldEvaluationTypeSettings + variants: List[PydanticObjectId] + version: str = Field("odmantic") + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluations" + + +class OldEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[OldEvaluationDB] + inputs: List[OldEvaluationScenarioInput] + outputs: List[OldEvaluationScenarioOutput] # EvaluationScenarioOutput + vote: Optional[str] + version: str = Field("odmantic") + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "evaluation_scenarios" + + +class OldCustomEvaluationDB(Document): + evaluation_name: str + python_code: str + version: str = Field("odmantic") + app: Link[AppDB] + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "custom_evaluations" + + +def modify_app_id_store( + app_id: str, + variant_ids: str, + evaluation_type: str, + app_keyvalue_store: Dict[str, Dict[str, List[str]]], +): + app_id_store = app_keyvalue_store.get(app_id, None) + if not app_id_store: + app_keyvalue_store[app_id] = {"variant_ids": [], "evaluation_types": []} + app_id_store = app_keyvalue_store[app_id] + + app_id_store_variant_ids = list(app_id_store["variant_ids"]) + if variant_ids not in list(app_id_store["variant_ids"]): + app_id_store_variant_ids.extend(variant_ids) + app_id_store["variant_ids"] = list(set(app_id_store_variant_ids)) + + app_id_store_evaluation_types = list(app_id_store["evaluation_types"]) + if evaluation_type not in app_id_store_evaluation_types: + app_id_store_evaluation_types.append(evaluation_type) + app_id_store["evaluation_types"] = list(set(app_id_store_evaluation_types)) + + +class Forward: + @free_fall_migration( + document_models=[ + AppDB, + OrganizationDB, + UserDB, + TestSetDB, + EvaluationDB, + OldEvaluationDB, + OldEvaluationScenarioDB, + EvaluationScenarioDB, + HumanEvaluationDB, + HumanEvaluationScenarioDB, + ] + ) + async def migrate_old_auto_evaluation_scenario_to_new_auto_evaluation_scenario( + self, session + ): + old_auto_scenarios = await OldEvaluationScenarioDB.find( + In( + OldEvaluationScenarioDB.evaluation.evaluation_type, + [ + "auto_exact_match", + "auto_similarity_match", + "auto_regex_test", + "auto_ai_critique", + "auto_custom_code_run", + "auto_webhook_test", + ], + ), + fetch_links=True, + ).to_list() + for old_scenario in old_auto_scenarios: + matching_evaluation = await EvaluationDB.find_one( + EvaluationDB.app.id == old_scenario.evaluation.app.id, + fetch_links=True, + ) + if matching_evaluation: + results = [ + EvaluationScenarioResult( + evaluator_config=PydanticObjectId(evaluator_config), + result=Result( + type="number" + if isinstance(old_scenario.score, int) + else "number" + if isinstance(old_scenario.score, float) + else "string" + if isinstance(old_scenario.score, str) + else "boolean" + if isinstance(old_scenario.score, bool) + else "any", + value=old_scenario.score, + ), + ) + for evaluator_config in matching_evaluation.evaluators_configs + ] + new_scenario = EvaluationScenarioDB( + user=matching_evaluation.user, + organization=matching_evaluation.organization, + evaluation=matching_evaluation, + variant_id=old_scenario.evaluation.variants[0], + inputs=[ + EvaluationScenarioInputDB( + name=input.input_name, + type=type(input.input_value).__name__, + value=input.input_value, + ) + for input in old_scenario.inputs + ], + outputs=[ + EvaluationScenarioOutputDB( + type=type(output.variant_output).__name__, + value=output.variant_output, + ) + for output in old_scenario.outputs + ], + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + evaluators_configs=matching_evaluation.evaluators_configs, + results=results, + ) + await new_scenario.insert(session=session) + + @free_fall_migration( + document_models=[ + AppDB, + OrganizationDB, + UserDB, + TestSetDB, + EvaluationDB, + OldEvaluationDB, + OldEvaluationScenarioDB, + EvaluationScenarioDB, + HumanEvaluationDB, + HumanEvaluationScenarioDB, + ] + ) + async def migrate_old_human_evaluation_scenario_to_new_human_evaluation_scenario( + self, session + ): + old_human_scenarios = await OldEvaluationScenarioDB.find( + In( + OldEvaluationScenarioDB.evaluation.evaluation_type, + ["human_a_b_testing", "single_model_test"], + ), + fetch_links=True, + ).to_list() + for old_scenario in old_human_scenarios: + matching_human_evaluation = await HumanEvaluationDB.find_one( + HumanEvaluationDB.app.id == old_scenario.evaluation.app.id, + fetch_links=True, + ) + if matching_human_evaluation: + scenario_inputs = [ + HumanEvaluationScenarioInput( + input_name=input.input_name, + input_value=input.input_value, + ) + for input in old_scenario.inputs + ] + scenario_outputs = [ + HumanEvaluationScenarioOutput( + variant_id=output.variant_id, + variant_output=output.variant_output, + ) + for output in old_scenario.outputs + ] + new_scenario = HumanEvaluationScenarioDB( + user=matching_human_evaluation.user, + organization=matching_human_evaluation.organization, + evaluation=matching_human_evaluation, + inputs=scenario_inputs, + outputs=scenario_outputs, + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + vote=old_scenario.vote, + score=old_scenario.score, + ) + await new_scenario.insert(session=session) + + +class Backward: + pass From d7c481a41c36c55ade6441eea77620795ddc85c8 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 15:57:41 +0100 Subject: [PATCH 200/267] Update - remove evaluation scenario logic from evaluation revamp migration --- .../20240110165900_evaluations_revamp.py | 196 +----------------- .../models/api/evaluation_model.py | 2 +- 2 files changed, 5 insertions(+), 193 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 95e60586f2..e1bb46ec7c 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -1,8 +1,6 @@ -import os from datetime import datetime from typing import Any, Dict, List, Optional -from pymongo import MongoClient from pydantic import BaseModel, Field from beanie import free_fall_migration, Document, Link, PydanticObjectId @@ -75,37 +73,11 @@ class Result(BaseModel): value: Any -class EvaluationScenarioResult(BaseModel): - evaluator_config: PydanticObjectId - result: Result - - class AggregatedResult(BaseModel): evaluator_config: PydanticObjectId result: Result -class EvaluationScenarioInputDB(BaseModel): - name: str - type: str - value: str - - -class EvaluationScenarioOutputDB(BaseModel): - type: str - value: Any - - -class HumanEvaluationScenarioInput(BaseModel): - input_name: str - input_value: str - - -class HumanEvaluationScenarioOutput(BaseModel): - variant_id: str - variant_output: str - - class HumanEvaluationDB(Document): app: Link[AppDB] organization: Link[OrganizationDB] @@ -121,24 +93,6 @@ class Settings: name = "human_evaluations" -class HumanEvaluationScenarioDB(Document): - user: Link[UserDB] - organization: Link[OrganizationDB] - evaluation: Link[HumanEvaluationDB] - inputs: List[HumanEvaluationScenarioInput] - outputs: List[HumanEvaluationScenarioOutput] - vote: Optional[str] - score: Optional[Any] - correct_answer: Optional[str] - created_at: Optional[datetime] = Field(default=datetime.utcnow()) - updated_at: Optional[datetime] = Field(default=datetime.utcnow()) - is_pinned: Optional[bool] - note: Optional[str] - - class Settings: - name = "human_evaluations_scenarios" - - class EvaluationDB(Document): app: Link[AppDB] organization: Link[OrganizationDB] @@ -155,25 +109,6 @@ class Settings: name = "new_evaluations" -class EvaluationScenarioDB(Document): - user: Link[UserDB] - organization: Link[OrganizationDB] - evaluation: Link[EvaluationDB] - variant_id: PydanticObjectId - inputs: List[EvaluationScenarioInputDB] - outputs: List[EvaluationScenarioOutputDB] - correct_answer: Optional[str] - is_pinned: Optional[bool] - note: Optional[str] - evaluators_configs: List[PydanticObjectId] - results: List[EvaluationScenarioResult] - created_at: datetime = Field(default=datetime.utcnow()) - updated_at: datetime = Field(default=datetime.utcnow()) - - class Settings: - name = "new_evaluation_scenarios" - - class OldEvaluationTypeSettings(BaseModel): similarity_threshold: Optional[float] regex_pattern: Optional[str] @@ -184,16 +119,6 @@ class OldEvaluationTypeSettings(BaseModel): evaluation_prompt_template: Optional[str] -class OldEvaluationScenarioInput(BaseModel): - input_name: str - input_value: str - - -class OldEvaluationScenarioOutput(BaseModel): - variant_id: str - variant_output: str - - class OldEvaluationDB(Document): app: Link[AppDB] organization: Link[OrganizationDB] @@ -211,25 +136,6 @@ class Settings: name = "evaluations" -class OldEvaluationScenarioDB(Document): - user: Link[UserDB] - organization: Link[OrganizationDB] - evaluation: Link[OldEvaluationDB] - inputs: List[OldEvaluationScenarioInput] - outputs: List[OldEvaluationScenarioOutput] # EvaluationScenarioOutput - vote: Optional[str] - version: str = Field("odmantic") - score: Optional[Any] - correct_answer: Optional[str] - created_at: Optional[datetime] = Field(default=datetime.utcnow()) - updated_at: Optional[datetime] = Field(default=datetime.utcnow()) - is_pinned: Optional[bool] - note: Optional[str] - - class Settings: - name = "evaluation_scenarios" - - class OldCustomEvaluationDB(Document): evaluation_name: str python_code: str @@ -354,7 +260,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): "human_a_b_testing", "single_model_test", ]: - auto_evaluator_configs.append(PydanticObjectId(evaluator_config.id)) + auto_evaluator_configs.append(evaluator_config.id) # STEP 3 (b): # In the case where the evaluator key is a human evaluator, @@ -365,7 +271,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): "single_model_test", ]: new_eval = HumanEvaluationDB( - app=PydanticObjectId(app_id), + app=old_eval.app, organization=old_eval.organization, user=old_eval.user, status=old_eval.status, @@ -381,111 +287,17 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): if auto_evaluator_configs is not None: for variant in app_id_store["variant_ids"]: new_eval = EvaluationDB( - app=PydanticObjectId(app_id), + app=old_eval.app, organization=old_eval.organization, user=old_eval.user, status=old_eval.status, testset=old_eval.testset, variant=PydanticObjectId(variant), evaluators_configs=auto_evaluator_configs, + aggregated_results=[], ) await new_eval.insert(session=session) - @free_fall_migration( - document_models=[ - AppDB, - OrganizationDB, - UserDB, - TestSetDB, - EvaluationDB, - OldEvaluationDB, - OldEvaluationScenarioDB, - EvaluationScenarioDB, - HumanEvaluationDB, - HumanEvaluationScenarioDB, - ] - ) - async def migrate_old_evaluation_scenario_to_new_evaluation_scenario(self, session): - old_scenarios = await OldEvaluationScenarioDB.find(fetch_links=True).to_list() - new_evaluations = await EvaluationDB.find(fetch_links=True).to_list() - new_human_evaluations = await HumanEvaluationDB.find(fetch_links=True).to_list() - - for old_scenario in old_scenarios: - matching_evaluations = [ - evaluation - for evaluation in new_evaluations - if old_scenario.evaluation.app == evaluation.app.id - ] - for evaluation in matching_evaluations: - results = [ - EvaluationScenarioResult( - evaluator_config=PydanticObjectId(evaluator_config), - result=old_scenario.score, - ) - for evaluator_config in evaluation.evaluators_configs - ] - new_scenario = EvaluationScenarioDB( - user=evaluation.user, - organization=evaluation.organization, - evaluation=evaluation, - variant_id=old_scenario.evaluation.variants[0], - inputs=[ - EvaluationScenarioInputDB( - name=input.input_name, - type=type(input.input_value).__name__, - value=input.input_value, - ) - for input in old_scenario.inputs - ], - outputs=[ - EvaluationScenarioOutputDB( - type=type(output.variant_output).__name__, - value=output.variant_output, - ) - for output in old_scenario.outputs - ], - correct_answer=old_scenario.correct_answer, - is_pinned=old_scenario.is_pinned, - note=old_scenario.note, - evaluators_configs=evaluation.evaluators_configs, - results=results, - ) - await new_scenario.insert(session=session) - - matching_human_evaluations = [ - evaluation - for evaluation in new_human_evaluations - if old_scenario.evaluation.app == evaluation.app.id - ] - for human_evaluation in matching_human_evaluations: - scenario_inputs = [ - HumanEvaluationScenarioInput( - input_name=input.input_name, - input_value=input.input_value, - ) - for input in old_scenario.inputs - ] - scenario_outputs = [ - HumanEvaluationScenarioOutput( - variant_id=output.variant_id, - variant_output=output.variant_output, - ) - for output in old_scenario.outputs - ] - new_scenario = HumanEvaluationScenarioDB( - user=human_evaluation.user, - organization=human_evaluation.organization, - evaluation=human_evaluation, - inputs=scenario_inputs, - outputs=scenario_outputs, - correct_answer=old_scenario.correct_answer, - is_pinned=old_scenario.is_pinned, - note=old_scenario.note, - vote=old_scenario.vote, - score=old_scenario.score, - ) - await new_scenario.insert(session=session) - class Backward: pass diff --git a/agenta-backend/agenta_backend/models/api/evaluation_model.py b/agenta-backend/agenta_backend/models/api/evaluation_model.py index 15a48e2fd4..f64e2b4833 100644 --- a/agenta-backend/agenta_backend/models/api/evaluation_model.py +++ b/agenta-backend/agenta_backend/models/api/evaluation_model.py @@ -114,7 +114,7 @@ class HumanEvaluation(BaseModel): app_id: str user_id: str user_username: str - evaluation_type: EvaluationType + evaluation_type: str variant_ids: List[str] variant_names: List[str] testset_id: str From b38eca004093511f97b84d0990c084edbd7b1fa8 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 16:00:37 +0100 Subject: [PATCH 201/267] Update - switch timestamp between 3rd and 4th migration files --- ...os_revamp.py => 20240112120721_evaluation_scenarios_revamp.py} | 0 ...ink.py => 20240113131802_change_odmantic_reference_to_link.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename agenta-backend/agenta_backend/migrations/{20240113131802_evaluation_scenarios_revamp.py => 20240112120721_evaluation_scenarios_revamp.py} (100%) rename agenta-backend/agenta_backend/migrations/{20240112120721_change_odmantic_reference_to_link.py => 20240113131802_change_odmantic_reference_to_link.py} (100%) diff --git a/agenta-backend/agenta_backend/migrations/20240113131802_evaluation_scenarios_revamp.py b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240113131802_evaluation_scenarios_revamp.py rename to agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_change_odmantic_reference_to_link.py b/agenta-backend/agenta_backend/migrations/20240113131802_change_odmantic_reference_to_link.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240112120721_change_odmantic_reference_to_link.py rename to agenta-backend/agenta_backend/migrations/20240113131802_change_odmantic_reference_to_link.py From 657ce5beed0cbe2e195ec927a72cace78b9a7f28 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 17:23:33 +0100 Subject: [PATCH 202/267] Update - modified steps to allow creation of human evaluations --- .../20240110165900_evaluations_revamp.py | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index e1bb46ec7c..9484542ab1 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -250,7 +250,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) - # STEP 3 (a): + # STEP 3: # Retrieve evaluator configs for app id auto_evaluator_configs: List[PydanticObjectId] = [] for evaluator_config in app_evaluator_configs: @@ -262,26 +262,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): ]: auto_evaluator_configs.append(evaluator_config.id) - # STEP 3 (b): - # In the case where the evaluator key is a human evaluator, - # Proceed to create the human evaluation with the evaluator config - for evaluator_config in app_evaluator_configs: - if evaluator_config.evaluator_key in [ - "human_a_b_testing", - "single_model_test", - ]: - new_eval = HumanEvaluationDB( - app=old_eval.app, - organization=old_eval.organization, - user=old_eval.user, - status=old_eval.status, - evaluation_type=evaluator_config.evaluator_key, - variants=app_id_store["variant_ids"], - testset=old_eval.testset, - ) - await new_eval.insert(session=session) - - # STEP 3 (c): + # STEP 4: # Proceed to create a single evaluation for every variant in the app_id_store # with the auto_evaluator_configs if auto_evaluator_configs is not None: @@ -298,6 +279,24 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): ) await new_eval.insert(session=session) + # STEP 5: + # Create the human evaluation + for old_evaluation in old_evaluations: + if old_evaluation.evaluation_type in [ + "human_a_b_testing", + "single_model_test", + ]: + new_eval = HumanEvaluationDB( + app=old_evaluation.app, + organization=old_evaluation.organization, + user=old_evaluation.user, + status=old_evaluation.status, + evaluation_type=old_evaluation.evaluation_type, + variants=old_evaluation.variants, + testset=old_evaluation.testset, + ) + await new_eval.insert(session=session) + class Backward: pass From dd8fe7b5919fb983b0cdeacb96cd121c6e762011 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 17:55:15 +0100 Subject: [PATCH 203/267] Update - broke down human evaluation scenarios migration logic --- ...40112120721_evaluation_scenarios_revamp.py | 85 +++++++++++++++---- 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py index d8ba377e79..da5cda1033 100644 --- a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py @@ -363,19 +363,17 @@ async def migrate_old_auto_evaluation_scenario_to_new_auto_evaluation_scenario( HumanEvaluationScenarioDB, ] ) - async def migrate_old_human_evaluation_scenario_to_new_human_evaluation_scenario( + async def migrate_old_human_a_b_evaluation_scenario_to_new_human_evaluation_scenario( self, session ): - old_human_scenarios = await OldEvaluationScenarioDB.find( - In( - OldEvaluationScenarioDB.evaluation.evaluation_type, - ["human_a_b_testing", "single_model_test"], - ), + old_human_ab_testing_scenarios = await OldEvaluationScenarioDB.find( + OldEvaluationScenarioDB.evaluation.evaluation_type == "human_a_b_testing", fetch_links=True, ).to_list() - for old_scenario in old_human_scenarios: + for ab_testing_scenario in old_human_ab_testing_scenarios: matching_human_evaluation = await HumanEvaluationDB.find_one( - HumanEvaluationDB.app.id == old_scenario.evaluation.app.id, + HumanEvaluationDB.app.id == ab_testing_scenario.evaluation.app.id, + HumanEvaluationDB.evaluation_type == "human_a_b_testing", fetch_links=True, ) if matching_human_evaluation: @@ -384,14 +382,14 @@ async def migrate_old_human_evaluation_scenario_to_new_human_evaluation_scenario input_name=input.input_name, input_value=input.input_value, ) - for input in old_scenario.inputs + for input in ab_testing_scenario.inputs ] scenario_outputs = [ HumanEvaluationScenarioOutput( variant_id=output.variant_id, variant_output=output.variant_output, ) - for output in old_scenario.outputs + for output in ab_testing_scenario.outputs ] new_scenario = HumanEvaluationScenarioDB( user=matching_human_evaluation.user, @@ -399,11 +397,68 @@ async def migrate_old_human_evaluation_scenario_to_new_human_evaluation_scenario evaluation=matching_human_evaluation, inputs=scenario_inputs, outputs=scenario_outputs, - correct_answer=old_scenario.correct_answer, - is_pinned=old_scenario.is_pinned, - note=old_scenario.note, - vote=old_scenario.vote, - score=old_scenario.score, + correct_answer=ab_testing_scenario.correct_answer, + is_pinned=ab_testing_scenario.is_pinned, + note=ab_testing_scenario.note, + vote=ab_testing_scenario.vote, + score=ab_testing_scenario.score, + ) + await new_scenario.insert(session=session) + + + @free_fall_migration( + document_models=[ + AppDB, + OrganizationDB, + UserDB, + TestSetDB, + EvaluationDB, + OldEvaluationDB, + OldEvaluationScenarioDB, + EvaluationScenarioDB, + HumanEvaluationDB, + HumanEvaluationScenarioDB, + ] + ) + async def migrate_old_human_single_model_evaluation_scenario_to_new_human_evaluation_scenario( + self, session + ): + old_human_single_model_scenarios = await OldEvaluationScenarioDB.find( + OldEvaluationScenarioDB.evaluation.evaluation_type == "single_model_test", + fetch_links=True, + ).to_list() + for single_model_scenario in old_human_single_model_scenarios: + matching_human_evaluation = await HumanEvaluationDB.find_one( + HumanEvaluationDB.app.id == single_model_scenario.evaluation.app.id, + HumanEvaluationDB.evaluation_type == "single_model_test", + fetch_links=True, + ) + if matching_human_evaluation: + scenario_inputs = [ + HumanEvaluationScenarioInput( + input_name=input.input_name, + input_value=input.input_value, + ) + for input in single_model_scenario.inputs + ] + scenario_outputs = [ + HumanEvaluationScenarioOutput( + variant_id=output.variant_id, + variant_output=output.variant_output, + ) + for output in single_model_scenario.outputs + ] + new_scenario = HumanEvaluationScenarioDB( + user=matching_human_evaluation.user, + organization=matching_human_evaluation.organization, + evaluation=matching_human_evaluation, + inputs=scenario_inputs, + outputs=scenario_outputs, + correct_answer=single_model_scenario.correct_answer, + is_pinned=single_model_scenario.is_pinned, + note=single_model_scenario.note, + vote=single_model_scenario.vote, + score=single_model_scenario.score, ) await new_scenario.insert(session=session) From ea18aa920e67d79cd970fe0fab8ff99558cd9dac Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 18:40:38 +0100 Subject: [PATCH 204/267] Update - modified step 2 logic in evaluations revamp migration --- .../20240110165900_evaluations_revamp.py | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 9484542ab1..5c4a643f4e 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -238,7 +238,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) - if evaluation_type not in ["custom_code_run", "auto_similarity_match"]: + if evaluation_type == "auto_exact_match": eval_config = EvaluatorConfigDB( app=old_eval.app, organization=old_eval.organization, @@ -250,6 +250,49 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) + if evaluation_type == "auto_regex_test": + eval_config = EvaluatorConfigDB( + app=old_eval.app, + organization=old_eval.organization, + user=old_eval.user, + name=f"{old_eval.app.app_name}_{evaluation_type}", + evaluator_key=evaluation_type, + settings_values={ + "regex_pattern": old_eval.evaluation_type_settings.regex_pattern, + "regex_should_match": old_eval.evaluation_type_settings.regex_should_match, + }, + ) + await eval_config.insert(session=session) + app_evaluator_configs.append(eval_config) + + if evaluation_type == "auto_webhook_test": + eval_config = EvaluatorConfigDB( + app=old_eval.app, + organization=old_eval.organization, + user=old_eval.user, + name=f"{old_eval.app.app_name}_{evaluation_type}", + evaluator_key=evaluation_type, + settings_values={ + "webhook_url": old_eval.evaluation_type_settings.webhook_url, + }, + ) + await eval_config.insert(session=session) + app_evaluator_configs.append(eval_config) + + if evaluation_type == "auto_ai_critique": + eval_config = EvaluatorConfigDB( + app=old_eval.app, + organization=old_eval.organization, + user=old_eval.user, + name=f"{old_eval.app.app_name}_{evaluation_type}", + evaluator_key=evaluation_type, + settings_values={ + "prompt_template": old_eval.evaluation_type_settings.evaluation_prompt_template + }, + ) + await eval_config.insert(session=session) + app_evaluator_configs.append(eval_config) + # STEP 3: # Retrieve evaluator configs for app id auto_evaluator_configs: List[PydanticObjectId] = [] From 508416c39cfde2a53f7fc32bd3173febe4945ed3 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 20:16:05 +0100 Subject: [PATCH 205/267] Update - cleanup and format evaluations revamp migration --- .../20240110165900_evaluations_revamp.py | 43 ++++++++++++------- ...40112120721_evaluation_scenarios_revamp.py | 1 - 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 5c4a643f4e..a4513d27d6 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -60,7 +60,7 @@ class EvaluatorConfigDB(Document): user: Link[UserDB] name: str evaluator_key: str - settings_values: Optional[Dict[str, Any]] = None + settings_values: Optional[Dict[str, Any]] created_at: datetime = Field(default=datetime.utcnow()) updated_at: datetime = Field(default=datetime.utcnow()) @@ -219,9 +219,9 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): user=old_eval.user, name=f"{old_eval.app.app_name}_{evaluation_type}", evaluator_key=f"auto_{evaluation_type}", - settings_values={} - if custom_code_evaluation is None - else {"code": custom_code_evaluation.python_code}, + settings_values=dict( + {"code": custom_code_evaluation.python_code} + ), ) await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) @@ -233,7 +233,11 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): user=old_eval.user, name=f"{old_eval.app.app_name}_{evaluation_type}", evaluator_key=evaluation_type, - settings_values={"similarity_threshold": 0.5}, + settings_values=dict( + { + "similarity_threshold": float(old_eval.evaluation_type_settings.similarity_threshold) + } + ), ) await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) @@ -245,7 +249,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): user=old_eval.user, name=f"{old_eval.app.app_name}_{evaluation_type}", evaluator_key=evaluation_type, - settings_values={}, + settings_values={} ) await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) @@ -257,10 +261,12 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): user=old_eval.user, name=f"{old_eval.app.app_name}_{evaluation_type}", evaluator_key=evaluation_type, - settings_values={ - "regex_pattern": old_eval.evaluation_type_settings.regex_pattern, - "regex_should_match": old_eval.evaluation_type_settings.regex_should_match, - }, + settings_values=dict( + { + "regex_pattern": old_eval.evaluation_type_settings.regex_pattern, + "regex_should_match": old_eval.evaluation_type_settings.regex_should_match, + } + ), ) await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) @@ -272,9 +278,12 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): user=old_eval.user, name=f"{old_eval.app.app_name}_{evaluation_type}", evaluator_key=evaluation_type, - settings_values={ - "webhook_url": old_eval.evaluation_type_settings.webhook_url, - }, + settings_values=dict( + { + "webhook_url": old_eval.evaluation_type_settings.webhook_url, + "webhook_body": {}, + } + ), ) await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) @@ -286,9 +295,11 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): user=old_eval.user, name=f"{old_eval.app.app_name}_{evaluation_type}", evaluator_key=evaluation_type, - settings_values={ - "prompt_template": old_eval.evaluation_type_settings.evaluation_prompt_template - }, + settings_values=dict( + { + "prompt_template": old_eval.evaluation_type_settings.evaluation_prompt_template + } + ), ) await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py index da5cda1033..8ad1f2a105 100644 --- a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py @@ -405,7 +405,6 @@ async def migrate_old_human_a_b_evaluation_scenario_to_new_human_evaluation_scen ) await new_scenario.insert(session=session) - @free_fall_migration( document_models=[ AppDB, From 2774ff923e074dd2e8b1d2fd6462c789dd8c9528 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 20:47:58 +0100 Subject: [PATCH 206/267] Update - modified logic to include evaluator result for evaluation results aggregation --- agenta-backend/agenta_backend/tasks/evaluations.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/agenta-backend/agenta_backend/tasks/evaluations.py b/agenta-backend/agenta_backend/tasks/evaluations.py index 5b0b861389..082b4bee96 100644 --- a/agenta-backend/agenta_backend/tasks/evaluations.py +++ b/agenta-backend/agenta_backend/tasks/evaluations.py @@ -86,7 +86,7 @@ def evaluate( # 2. Initialize vars evaluators_aggregated_data = { - evaluator_config_db.id: { + str(evaluator_config_db.id): { "evaluator_key": evaluator_config.evaluator_key, "results": [], } @@ -115,7 +115,7 @@ def evaluate( self.update_state(state=states.FAILURE) raise ValueError("Length of csv data and app_outputs are not the same") - return + for data_point, app_output in zip(testset_db.csvdata, app_outputs): # 2. We prepare the inputs logger.debug(f"Preparing inputs for data point: {data_point}") @@ -149,6 +149,10 @@ def evaluate( lm_providers_keys=lm_providers_keys, ) + # Update evaluators aggregated data + evaluator_results: List[Result] = evaluators_aggregated_data[str(evaluator_config_db.id)]["results"] + evaluator_results.append(result) + result_object = EvaluationScenarioResult( evaluator_config=evaluator_config_db.id, result=result, From 01ce41efbcd9274bcac3f1fb5e0cd95507c7546e Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 20:48:37 +0100 Subject: [PATCH 207/267] Update - modified evaluator config db model --- agenta-backend/agenta_backend/models/db_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/models/db_models.py b/agenta-backend/agenta_backend/models/db_models.py index 68d15450b8..11ad909077 100644 --- a/agenta-backend/agenta_backend/models/db_models.py +++ b/agenta-backend/agenta_backend/models/db_models.py @@ -200,7 +200,7 @@ class EvaluatorConfigDB(Document): user: Link[UserDB] name: str evaluator_key: str - settings_values: Optional[Dict[str, Any]] = None + settings_values: Dict[str, Any] = Field(default=dict) created_at: datetime = Field(default=datetime.utcnow()) updated_at: datetime = Field(default=datetime.utcnow()) From 85ae65f27e5e5297f26a70ba59d043ff2cd1e486 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 23:26:33 +0100 Subject: [PATCH 208/267] Update - tiny cleanup and format --- .../migrations/20240110165900_evaluations_revamp.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index a4513d27d6..2a99a65a84 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -162,7 +162,7 @@ def modify_app_id_store( app_id_store = app_keyvalue_store[app_id] app_id_store_variant_ids = list(app_id_store["variant_ids"]) - if variant_ids not in list(app_id_store["variant_ids"]): + if variant_ids not in app_id_store_variant_ids: app_id_store_variant_ids.extend(variant_ids) app_id_store["variant_ids"] = list(set(app_id_store_variant_ids)) @@ -235,7 +235,9 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): evaluator_key=evaluation_type, settings_values=dict( { - "similarity_threshold": float(old_eval.evaluation_type_settings.similarity_threshold) + "similarity_threshold": float( + old_eval.evaluation_type_settings.similarity_threshold + ) } ), ) @@ -249,7 +251,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): user=old_eval.user, name=f"{old_eval.app.app_name}_{evaluation_type}", evaluator_key=evaluation_type, - settings_values={} + settings_values={}, ) await eval_config.insert(session=session) app_evaluator_configs.append(eval_config) From d989fbb8bf00c575348043eeec1d25d1c812ee25 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 23:27:17 +0100 Subject: [PATCH 209/267] Update - tiny cleanup and format --- agenta-backend/agenta_backend/tasks/evaluations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/tasks/evaluations.py b/agenta-backend/agenta_backend/tasks/evaluations.py index 082b4bee96..6bb45e0924 100644 --- a/agenta-backend/agenta_backend/tasks/evaluations.py +++ b/agenta-backend/agenta_backend/tasks/evaluations.py @@ -150,7 +150,9 @@ def evaluate( ) # Update evaluators aggregated data - evaluator_results: List[Result] = evaluators_aggregated_data[str(evaluator_config_db.id)]["results"] + evaluator_results: List[Result] = evaluators_aggregated_data[ + str(evaluator_config_db.id) + ]["results"] evaluator_results.append(result) result_object = EvaluationScenarioResult( From 7943810ad675b3b2ab9f82cfdfdb9a73fe3e2b14 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 23:27:39 +0100 Subject: [PATCH 210/267] Feat - created migration logic to aggregated evaluation scenarios results --- ...4909_new_evaluation_results_aggregation.py | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 agenta-backend/agenta_backend/migrations/20240113204909_new_evaluation_results_aggregation.py diff --git a/agenta-backend/agenta_backend/migrations/20240113204909_new_evaluation_results_aggregation.py b/agenta-backend/agenta_backend/migrations/20240113204909_new_evaluation_results_aggregation.py new file mode 100644 index 0000000000..884b419881 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/20240113204909_new_evaluation_results_aggregation.py @@ -0,0 +1,257 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field +from beanie import free_fall_migration, Document, Link, PydanticObjectId + + +class OrganizationDB(Document): + name: str = Field(default="agenta") + description: str = Field(default="") + type: Optional[str] + owner: str # user id + members: Optional[List[PydanticObjectId]] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "organizations" + + +class UserDB(Document): + uid: str = Field(default="0", unique=True, index=True) + username: str = Field(default="agenta") + email: str = Field(default="demo@agenta.ai", unique=True) + organizations: Optional[List[PydanticObjectId]] = [] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "users" + + +class AppDB(Document): + app_name: str + organization: Link[OrganizationDB] + user: Link[UserDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "app_db" + + +class TestSetDB(Document): + name: str + app: Link[AppDB] + csvdata: List[Dict[str, str]] + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "testsets" + + +class Result(BaseModel): + type: str + value: Any + + +class EvaluationScenarioResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class AggregatedResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class EvaluationScenarioInputDB(BaseModel): + name: str + type: str + value: str + + +class EvaluationScenarioOutputDB(BaseModel): + type: str + value: Any + + +class EvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str = Field(default="EVALUATION_INITIALIZED") + testset: Link[TestSetDB] + variant: PydanticObjectId + evaluators_configs: List[PydanticObjectId] + aggregated_results: List[AggregatedResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "new_evaluations" + + +class EvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[EvaluationDB] + variant_id: PydanticObjectId + inputs: List[EvaluationScenarioInputDB] + outputs: List[EvaluationScenarioOutputDB] + correct_answer: Optional[str] + is_pinned: Optional[bool] + note: Optional[str] + evaluators_configs: List[PydanticObjectId] + results: List[EvaluationScenarioResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "new_evaluation_scenarios" + + +def prepare_evaluation_keyvalue_store( + evaluation_id: str, evaluator_id: str, evaluation_keyvalue_store: Dict +) -> Dict[str, Dict[str, Any]]: + """ + Construct a key-value store to saves results based on a evaluator config in an evaluation + + Args: + evaluation_id (str): ID of evaluation + evaluator_id (str): ID of evaluator config + evaluation_keyvalue_store (Dict): evaluation keyvalue store + + Returns: + Dict[str, Dict[str, Any]]: {"evaluation_id": {"evaluation_config_id": {"results": [Result("type": str, "value": Any)]}}} + """ + + if evaluation_id not in evaluation_keyvalue_store: + evaluation_keyvalue_store[evaluation_id] = {} + + if evaluator_id not in evaluation_keyvalue_store[evaluation_id]: + evaluation_keyvalue_store[evaluation_id][evaluator_id] = {"results": []} + + return evaluation_keyvalue_store + + +def get_numeric_value(value: Any): + """ + Converts the given value to a numeric representation, with specific + conversions for strings such as 'correct', 'wrong', 'true', and 'false'. + """ + + if isinstance(value, str): + if value.lower() == "correct": + return 1 + elif value.lower() == "wrong": + return 0 + elif value.lower() == "true": + return float(True) + elif value.lower() == "false": + return float(False) + else: + return float(value) + return 0 + + +def aggregate_evaluator_results( + evaluators_aggregated_data: dict, +) -> List[AggregatedResult]: + aggregated_results = [] + for config_id, evaluator_store in evaluators_aggregated_data.items(): + results: List[EvaluationScenarioResult] = evaluator_store.get("results", []) + if len(results) >= 1: + values = [get_numeric_value(result.result.value) for result in results] + average_value = sum(values) / len(values) + else: + average_value = 0 + + aggregated_result = AggregatedResult( + evaluator_config=PydanticObjectId(config_id), + result=Result(type="number", value=round(average_value, 4)), + ) + aggregated_results.append(aggregated_result) + return aggregated_results + + +def modify_evaluation_scenario_store( + evaluator_id: str, + result: Result, + evaluation_keyvalue_store: Dict[str, Dict[str, List[Any]]], +): + """ + Updates an evaluation scenario store by adding a result to the list of results for a + specific evaluation and evaluator. + + Args: + evaluator_id (str): ID of evaluator config + result: The evaluation result that needs to be added to the evaluation_results list + evaluation_keyvalue_store: The store that holds the evaluation data + """ + + evaluation_evaluator_config_store = evaluation_keyvalue_store[evaluator_id] + evaluation_results = list(evaluation_evaluator_config_store["results"]) + if result not in evaluation_results: + evaluation_results.append(result) + evaluation_evaluator_config_store["results"] = list(evaluation_results) + + +class Forward: + @free_fall_migration( + document_models=[ + AppDB, + OrganizationDB, + UserDB, + TestSetDB, + EvaluationDB, + EvaluationScenarioDB, + ] + ) + async def aggregate_new_evaluation_with_evaluation_scenario_results(self, session): + # STEP 1: + # Create a key-value store that saves all the evaluator configs & results for a particular evaluation id + # Example: {"evaluation_id": {"evaluation_config_id": {"results": [Result("type": str, "value": Any)]}}} + evaluation_keyvalue_store = {} + new_auto_evaluations = await EvaluationDB.find().to_list() + for auto_evaluation in new_auto_evaluations: + for evaluator_config in auto_evaluation.evaluators_configs: + evaluation_keyvalue_store = prepare_evaluation_keyvalue_store( + str(auto_evaluation.id), + str(evaluator_config), + evaluation_keyvalue_store, + ) + + # STEP 2: + # Update the evaluation key-value store + new_auto_evaluation_scenarios = await EvaluationScenarioDB.find( + fetch_links=True + ).to_list() + for auto_evaluation in new_auto_evaluation_scenarios: + evaluation_id = str(auto_evaluation.evaluation.id) + evaluation_store = evaluation_keyvalue_store[evaluation_id] + configs_with_results = zip( + auto_evaluation.evaluators_configs, auto_evaluation.results + ) + for evaluator, result in configs_with_results: + modify_evaluation_scenario_store( + str(evaluator), result, evaluation_store + ) + + # STEP 3: + # Modify the evaluations with the aggregated results from the keyvalue store + for auto_evaluation in new_auto_evaluations: + aggregated_results = aggregate_evaluator_results( + evaluation_keyvalue_store[str(auto_evaluation.id)] + ) + auto_evaluation.aggregated_results = aggregated_results + auto_evaluation.updated_at = datetime.utcnow().isoformat() + await auto_evaluation.save() + + +class Backward: + pass From 42eb6c9fda7fb8e16a0195226ad4b915802f9be3 Mon Sep 17 00:00:00 2001 From: Abram Date: Sat, 13 Jan 2024 23:29:06 +0100 Subject: [PATCH 211/267] Update - swtich timestamp between migration 4 and 5 --- ...on.py => 20240113131802_new_evaluation_results_aggregation.py} | 0 ...ink.py => 20240113204909_change_odmantic_reference_to_link.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename agenta-backend/agenta_backend/migrations/{20240113204909_new_evaluation_results_aggregation.py => 20240113131802_new_evaluation_results_aggregation.py} (100%) rename agenta-backend/agenta_backend/migrations/{20240113131802_change_odmantic_reference_to_link.py => 20240113204909_change_odmantic_reference_to_link.py} (100%) diff --git a/agenta-backend/agenta_backend/migrations/20240113204909_new_evaluation_results_aggregation.py b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240113204909_new_evaluation_results_aggregation.py rename to agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py diff --git a/agenta-backend/agenta_backend/migrations/20240113131802_change_odmantic_reference_to_link.py b/agenta-backend/agenta_backend/migrations/20240113204909_change_odmantic_reference_to_link.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240113131802_change_odmantic_reference_to_link.py rename to agenta-backend/agenta_backend/migrations/20240113204909_change_odmantic_reference_to_link.py From f0570952132d7aeba24af0d9748d6c21ed751a5c Mon Sep 17 00:00:00 2001 From: Abram Date: Sun, 14 Jan 2024 00:00:09 +0100 Subject: [PATCH 212/267] Update - added status to evaluation --- .../20240113131802_new_evaluation_results_aggregation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py index 884b419881..935c6c66be 100644 --- a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py +++ b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py @@ -248,6 +248,7 @@ async def aggregate_new_evaluation_with_evaluation_scenario_results(self, sessio aggregated_results = aggregate_evaluator_results( evaluation_keyvalue_store[str(auto_evaluation.id)] ) + auto_evaluation.status = "EVALUATION_FINISHED" auto_evaluation.aggregated_results = aggregated_results auto_evaluation.updated_at = datetime.utcnow().isoformat() await auto_evaluation.save() From 4574b1b10ffe5e58ce6d8f177c58114d1ccf057c Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Sun, 14 Jan 2024 12:43:34 +0100 Subject: [PATCH 213/267] formatter fix --- .../src/pages/apps/[app_id]/testsets/new/upload/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx b/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx index 29f7c47337..139c45ffdc 100644 --- a/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx +++ b/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx @@ -70,8 +70,8 @@ export default function AddANewTestset() { router.push(`/apps/${appId}/testsets`) } catch (e: any) { if ( - e?.response?.data?.detail?.find( - (item: GenericObject) => item?.loc?.includes("csvdata"), + e?.response?.data?.detail?.find((item: GenericObject) => + item?.loc?.includes("csvdata"), ) ) message.error(malformedFileError) From 0f78ebf1b9e3d09482bbc97af569b5e86713bdc5 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Sun, 14 Jan 2024 12:44:14 +0100 Subject: [PATCH 214/267] formatter fix --- .../src/pages/apps/[app_id]/testsets/new/upload/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx b/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx index 29f7c47337..139c45ffdc 100644 --- a/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx +++ b/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx @@ -70,8 +70,8 @@ export default function AddANewTestset() { router.push(`/apps/${appId}/testsets`) } catch (e: any) { if ( - e?.response?.data?.detail?.find( - (item: GenericObject) => item?.loc?.includes("csvdata"), + e?.response?.data?.detail?.find((item: GenericObject) => + item?.loc?.includes("csvdata"), ) ) message.error(malformedFileError) From f906368cf31d798052c1ac5755aff1edd834589c Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 15 Jan 2024 09:31:05 +0100 Subject: [PATCH 215/267] export button always activated --- .../src/components/EvaluationTable/ABTestingEvaluationTable.tsx | 2 +- .../components/EvaluationTable/SingleModelEvaluationTable.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx index fb6331e2a8..8c4ca6fc27 100644 --- a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx @@ -382,7 +382,7 @@ const ABTestingEvaluationTable: React.FC = ({ exportABTestingEvaluationData(evaluation, rows)} - disabled={evaluationStatus !== EvaluationFlow.EVALUATION_FINISHED} + disabled={false} > Export results diff --git a/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx index a1ae8ecd76..efa7f1efcd 100644 --- a/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx @@ -449,7 +449,7 @@ const SingleModelEvaluationTable: React.FC = ({ exportSingleModelEvaluationData(evaluation, rows)} - disabled={evaluationStatus !== EvaluationFlow.EVALUATION_FINISHED} + disabled={false} > Export results From f841c765f1a2c7401466275b4403184837acdc7c Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Mon, 15 Jan 2024 09:50:53 +0100 Subject: [PATCH 216/267] fix format --- .../src/pages/apps/[app_id]/testsets/new/upload/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx b/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx index 29f7c47337..139c45ffdc 100644 --- a/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx +++ b/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx @@ -70,8 +70,8 @@ export default function AddANewTestset() { router.push(`/apps/${appId}/testsets`) } catch (e: any) { if ( - e?.response?.data?.detail?.find( - (item: GenericObject) => item?.loc?.includes("csvdata"), + e?.response?.data?.detail?.find((item: GenericObject) => + item?.loc?.includes("csvdata"), ) ) message.error(malformedFileError) From 7e3083ba5c7d7a336dd4fcdd46f9e42e80fb5cdd Mon Sep 17 00:00:00 2001 From: Abram Date: Mon, 15 Jan 2024 11:39:24 +0100 Subject: [PATCH 217/267] Update - modified logic to get_numeric_value for aggregated results --- .../20240113131802_new_evaluation_results_aggregation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py index 935c6c66be..09856eda80 100644 --- a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py +++ b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py @@ -155,7 +155,10 @@ def get_numeric_value(value: Any): elif value.lower() == "false": return float(False) else: - return float(value) + try: + return float(value) + except ValueError: + return 0 return 0 From 3a82c5b9ee00245b9c3b4debced968b44488722b Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Mon, 15 Jan 2024 12:27:35 +0100 Subject: [PATCH 218/267] feat: export answer and notes --- .../EvaluationTable/ABTestingEvaluationTable.tsx | 8 +++++++- agenta-web/src/lib/helpers/evaluate.ts | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx index 8c4ca6fc27..777729b31a 100644 --- a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx @@ -381,7 +381,13 @@ const ABTestingEvaluationTable: React.FC = ({ Run All exportABTestingEvaluationData(evaluation, rows)} + onClick={() => + exportABTestingEvaluationData( + evaluation, + evaluationScenarios, + rows, + ) + } disabled={false} > Export results diff --git a/agenta-web/src/lib/helpers/evaluate.ts b/agenta-web/src/lib/helpers/evaluate.ts index 5311b1e7c2..b0caba4cce 100644 --- a/agenta-web/src/lib/helpers/evaluate.ts +++ b/agenta-web/src/lib/helpers/evaluate.ts @@ -1,5 +1,5 @@ import {HumanEvaluationListTableDataType} from "@/components/Evaluations/HumanEvaluationResult" -import {Evaluation, GenericObject, Variant} from "../Types" +import {Evaluation, EvaluationScenario, GenericObject, Variant} from "../Types" import {convertToCsv, downloadCsv} from "./fileManipulations" export const exportExactEvaluationData = (evaluation: Evaluation, rows: GenericObject[]) => { @@ -63,7 +63,11 @@ export const exportAICritiqueEvaluationData = (evaluation: Evaluation, rows: Gen downloadCsv(csvData, filename) } -export const exportABTestingEvaluationData = (evaluation: Evaluation, rows: GenericObject[]) => { +export const exportABTestingEvaluationData = ( + evaluation: Evaluation, + scenarios: EvaluationScenario[], + rows: GenericObject[], +) => { const exportRow = rows.map((data, ix) => { return { ["Inputs"]: @@ -78,6 +82,8 @@ export const exportABTestingEvaluationData = (evaluation: Evaluation, rows: Gene ["Vote"]: evaluation.variants.find((v: Variant) => v.variantId === data.vote)?.variantName || data.vote, + ["Expected answer"]: scenarios[ix]?.correctAnswer, + ["Additional notes"]: scenarios[ix]?.note, } }) const exportCol = Object.keys(exportRow[0]) From a7590e6f5ea138e6dc0a2b845c031c77329dfc56 Mon Sep 17 00:00:00 2001 From: Abram Date: Mon, 15 Jan 2024 13:22:06 +0100 Subject: [PATCH 219/267] Update - modify logic to assign evaluations to their respective users --- .../20240110165900_evaluations_revamp.py | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 2a99a65a84..689b277865 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -205,6 +205,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): # based on the evaluation types available for app_id, app_id_store in app_keyvalue_store.items(): app_evaluator_configs: List[EvaluatorConfigDB] = [] + app_db = await AppDB.find_one(AppDB.id == PydanticObjectId(app_id)) for evaluation_type in app_id_store[ "evaluation_types" ]: # the values in this case are the evaluation type @@ -214,10 +215,10 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): if evaluation_type == "custom_code_run": for custom_code_evaluation in custom_code_evaluations: eval_config = EvaluatorConfigDB( - app=old_eval.app, - organization=old_eval.organization, - user=old_eval.user, - name=f"{old_eval.app.app_name}_{evaluation_type}", + app=app_db, + organization=app_db.organization, + user=app_db.user, + name=f"{app_db.app_name}_{evaluation_type}", evaluator_key=f"auto_{evaluation_type}", settings_values=dict( {"code": custom_code_evaluation.python_code} @@ -228,10 +229,10 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): if evaluation_type == "auto_similarity_match": eval_config = EvaluatorConfigDB( - app=old_eval.app, - organization=old_eval.organization, - user=old_eval.user, - name=f"{old_eval.app.app_name}_{evaluation_type}", + app=app_db, + organization=app_db.organization, + user=app_db.user, + name=f"{app_db.app_name}_{evaluation_type}", evaluator_key=evaluation_type, settings_values=dict( { @@ -246,10 +247,10 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): if evaluation_type == "auto_exact_match": eval_config = EvaluatorConfigDB( - app=old_eval.app, - organization=old_eval.organization, - user=old_eval.user, - name=f"{old_eval.app.app_name}_{evaluation_type}", + app=app_db, + organization=app_db.organization, + user=app_db.user, + name=f"{app_db.app_name}_{evaluation_type}", evaluator_key=evaluation_type, settings_values={}, ) @@ -258,10 +259,10 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): if evaluation_type == "auto_regex_test": eval_config = EvaluatorConfigDB( - app=old_eval.app, - organization=old_eval.organization, - user=old_eval.user, - name=f"{old_eval.app.app_name}_{evaluation_type}", + app=app_db, + organization=app_db.organization, + user=app_db.user, + name=f"{app_db.app_name}_{evaluation_type}", evaluator_key=evaluation_type, settings_values=dict( { @@ -275,10 +276,10 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): if evaluation_type == "auto_webhook_test": eval_config = EvaluatorConfigDB( - app=old_eval.app, - organization=old_eval.organization, - user=old_eval.user, - name=f"{old_eval.app.app_name}_{evaluation_type}", + app=app_db, + organization=app_db.organization, + user=app_db.user, + name=f"{app_db.app_name}_{evaluation_type}", evaluator_key=evaluation_type, settings_values=dict( { @@ -292,10 +293,10 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): if evaluation_type == "auto_ai_critique": eval_config = EvaluatorConfigDB( - app=old_eval.app, - organization=old_eval.organization, - user=old_eval.user, - name=f"{old_eval.app.app_name}_{evaluation_type}", + app=app_db, + organization=app_db.organization, + user=app_db.user, + name=f"{app_db.app_name}_{evaluation_type}", evaluator_key=evaluation_type, settings_values=dict( { @@ -324,9 +325,9 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): if auto_evaluator_configs is not None: for variant in app_id_store["variant_ids"]: new_eval = EvaluationDB( - app=old_eval.app, - organization=old_eval.organization, - user=old_eval.user, + app=app_db, + organization=app_db.organization, + user=app_db.user, status=old_eval.status, testset=old_eval.testset, variant=PydanticObjectId(variant), From 6d89c0dd38d43ae4a338835b653445bf105c9c5a Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Mon, 15 Jan 2024 15:52:41 +0100 Subject: [PATCH 220/267] dynamic column input for csv --- agenta-web/src/lib/helpers/evaluate.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/agenta-web/src/lib/helpers/evaluate.ts b/agenta-web/src/lib/helpers/evaluate.ts index b0caba4cce..7c29834e13 100644 --- a/agenta-web/src/lib/helpers/evaluate.ts +++ b/agenta-web/src/lib/helpers/evaluate.ts @@ -69,10 +69,15 @@ export const exportABTestingEvaluationData = ( rows: GenericObject[], ) => { const exportRow = rows.map((data, ix) => { + const inputColumns = data.inputs.reduce( + (columns: any, input: {input_name: string; input_value: string}) => { + columns[`${input.input_name}`] = input.input_value + return columns + }, + {}, + ) return { - ["Inputs"]: - evaluation.testset.csvdata[ix]?.[evaluation.testset.testsetChatColumn] || - data.inputs[0].input_value, + ...inputColumns, [`App Variant ${evaluation.variants[0].variantName} Output 0`]: data?.columnData0 ? data?.columnData0 : data.outputs[0]?.variant_output, From 05a143647340a64c4a32044c12c92b5f44aec1cf Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Mon, 15 Jan 2024 16:18:29 +0100 Subject: [PATCH 221/267] conditional check for expected answer --- agenta-web/src/lib/helpers/evaluate.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agenta-web/src/lib/helpers/evaluate.ts b/agenta-web/src/lib/helpers/evaluate.ts index 7c29834e13..f4d8642edb 100644 --- a/agenta-web/src/lib/helpers/evaluate.ts +++ b/agenta-web/src/lib/helpers/evaluate.ts @@ -87,7 +87,8 @@ export const exportABTestingEvaluationData = ( ["Vote"]: evaluation.variants.find((v: Variant) => v.variantId === data.vote)?.variantName || data.vote, - ["Expected answer"]: scenarios[ix]?.correctAnswer, + ["Expected answer"]: + scenarios[ix]?.correctAnswer || evaluation.testset.csvdata[ix].correct_answer, ["Additional notes"]: scenarios[ix]?.note, } }) From cf6157d760a80429e3e26807e6899f4c229bae5f Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Mon, 15 Jan 2024 16:36:38 +0100 Subject: [PATCH 222/267] added expected answer and note to csv and dynamic col inputs to single model --- .../SingleModelEvaluationTable.tsx | 8 +++++++- agenta-web/src/lib/helpers/evaluate.ts | 20 +++++++++++++++---- rabbitmq_data/.erlang.cookie | 1 + 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 rabbitmq_data/.erlang.cookie diff --git a/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx index efa7f1efcd..70d12a7dc3 100644 --- a/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx @@ -448,7 +448,13 @@ const SingleModelEvaluationTable: React.FC = ({ Run All exportSingleModelEvaluationData(evaluation, rows)} + onClick={() => + exportSingleModelEvaluationData( + evaluation, + evaluationScenarios, + rows, + ) + } disabled={false} > Export results diff --git a/agenta-web/src/lib/helpers/evaluate.ts b/agenta-web/src/lib/helpers/evaluate.ts index f4d8642edb..31ce851ec6 100644 --- a/agenta-web/src/lib/helpers/evaluate.ts +++ b/agenta-web/src/lib/helpers/evaluate.ts @@ -99,17 +99,29 @@ export const exportABTestingEvaluationData = ( downloadCsv(csvData, filename) } -export const exportSingleModelEvaluationData = (evaluation: Evaluation, rows: GenericObject[]) => { +export const exportSingleModelEvaluationData = ( + evaluation: Evaluation, + scenarios: EvaluationScenario[], + rows: GenericObject[], +) => { const exportRow = rows.map((data, ix) => { + const inputColumns = data.inputs.reduce( + (columns: any, input: {input_name: string; input_value: string}) => { + columns[`${input.input_name}`] = input.input_value + return columns + }, + {}, + ) const numericScore = parseInt(data.score) return { - ["Inputs"]: - evaluation.testset.csvdata[ix]?.[evaluation.testset.testsetChatColumn] || - data.inputs[0].input_value, + ...inputColumns, [`App Variant ${evaluation.variants[0].variantName} Output 0`]: data?.columnData0 ? data?.columnData0 : data.outputs[0]?.variant_output, ["Score"]: isNaN(numericScore) ? "-" : numericScore, + ["Expected answer"]: + scenarios[ix]?.correctAnswer || evaluation.testset.csvdata[ix].correct_answer, + ["Additional notes"]: scenarios[ix]?.note, } }) const exportCol = Object.keys(exportRow[0]) diff --git a/rabbitmq_data/.erlang.cookie b/rabbitmq_data/.erlang.cookie new file mode 100644 index 0000000000..f9807e6a0c --- /dev/null +++ b/rabbitmq_data/.erlang.cookie @@ -0,0 +1 @@ +IEMBREZETCGSGJBJAKVJ \ No newline at end of file From 7f1ef7baf73cefffd90093b10c4eec24777813f0 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Tue, 16 Jan 2024 01:45:17 +0100 Subject: [PATCH 223/267] removed rabbitmd_data --- rabbitmq_data/.erlang.cookie | 1 - 1 file changed, 1 deletion(-) delete mode 100644 rabbitmq_data/.erlang.cookie diff --git a/rabbitmq_data/.erlang.cookie b/rabbitmq_data/.erlang.cookie deleted file mode 100644 index f9807e6a0c..0000000000 --- a/rabbitmq_data/.erlang.cookie +++ /dev/null @@ -1 +0,0 @@ -IEMBREZETCGSGJBJAKVJ \ No newline at end of file From 517ebf897f68f864b4cb2cd7183473881c9b4c23 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Tue, 16 Jan 2024 15:56:42 +0500 Subject: [PATCH 224/267] refactoring --- agenta-web/dev.Dockerfile | 60 ++++----- .../components/Playground/Views/TestView.tsx | 126 ++++++------------ agenta-web/src/hooks/useStateCallback.ts | 10 +- agenta-web/src/lib/services/api.ts | 12 +- 4 files changed, 85 insertions(+), 123 deletions(-) diff --git a/agenta-web/dev.Dockerfile b/agenta-web/dev.Dockerfile index 6f86dbd847..91060e8aa6 100644 --- a/agenta-web/dev.Dockerfile +++ b/agenta-web/dev.Dockerfile @@ -1,37 +1,37 @@ FROM node:18-alpine -WORKDIR /app +# WORKDIR /app -# Install dependencies based on the preferred package manager -COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ -RUN \ - if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ - elif [ -f package-lock.json ]; then npm i; \ - elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ - # Allow install without lockfile, so example works even without Node.js installed locally - else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ - fi +# # Install dependencies based on the preferred package manager +# COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +# RUN \ +# if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ +# elif [ -f package-lock.json ]; then npm i; \ +# elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ +# # Allow install without lockfile, so example works even without Node.js installed locally +# else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ +# fi -COPY src ./src -COPY public ./public -COPY next.config.js . -COPY tsconfig.json . -COPY postcss.config.js . -COPY .env . -RUN if [ -f .env.local ]; then cp .env.local .; fi -# used in cloud -COPY sentry.* . -# Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry -# Uncomment the following line to disable telemetry at run time -# ENV NEXT_TELEMETRY_DISABLED 1 +# COPY src ./src +# COPY public ./public +# COPY next.config.js . +# COPY tsconfig.json . +# COPY postcss.config.js . +# COPY .env . +# RUN if [ -f .env.local ]; then cp .env.local .; fi +# # used in cloud +# COPY sentry.* . +# # Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry +# # Uncomment the following line to disable telemetry at run time +# # ENV NEXT_TELEMETRY_DISABLED 1 -# Note: Don't expose ports here, Compose will handle that for us +# # Note: Don't expose ports here, Compose will handle that for us -# Start Next.js in development mode based on the preferred package manager -CMD \ - if [ -f yarn.lock ]; then yarn dev; \ - elif [ -f package-lock.json ]; then npm run dev; \ - elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ - else yarn dev; \ - fi +# # Start Next.js in development mode based on the preferred package manager +# CMD \ +# if [ -f yarn.lock ]; then yarn dev; \ +# elif [ -f package-lock.json ]; then npm run dev; \ +# elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ +# else yarn dev; \ +# fi diff --git a/agenta-web/src/components/Playground/Views/TestView.tsx b/agenta-web/src/components/Playground/Views/TestView.tsx index 2fd02bc0e9..0e63e18b99 100644 --- a/agenta-web/src/components/Playground/Views/TestView.tsx +++ b/agenta-web/src/components/Playground/Views/TestView.tsx @@ -349,31 +349,30 @@ const App: React.FC = ({ } const handleRun = async (index: number) => { - if (abortControllersRef.current[index]) { - abortControllersRef.current[index].abort() - } - const controller = new AbortController() abortControllersRef.current[index] = controller try { const testItem = testList[index] if (compareMode && !isRunning[index]) { - setIsRunning( - (prevState) => { - const newState = [...prevState] - newState[index] = true - return newState - }, - () => { - document - .querySelectorAll(`.testview-run-button-${testItem._id}`) - .forEach((btn) => { - if (btn.parentElement?.id !== variant.variantId) { - ;(btn as HTMLButtonElement).click() - } - }) - }, - ) + let called = false + const callback = () => { + if (called) return + called = true + document + .querySelectorAll(`.testview-run-button-${testItem._id}`) + .forEach((btn) => { + if (btn.parentElement?.id !== variant.variantId) { + ;(btn as HTMLButtonElement).click() + } + }) + } + + setIsRunning((prevState) => { + const newState = [...prevState] + newState[index] = true + return newState + }, callback) + setTimeout(callback, 300) } setResultForIndex(LOADING_TEXT, index) @@ -385,6 +384,7 @@ const App: React.FC = ({ variant.baseId || "", isChatVariant ? testItem.chat : [], controller.signal, + true, ) // check if res is an object or string @@ -401,6 +401,13 @@ const App: React.FC = ({ } catch (e) { if (!controller.signal.aborted) { setResultForIndex(`❌ ${getErrorMessage(e)}`, index) + } else { + setResultForIndex("", index) + setAdditionalDataList((prev) => { + const newDataList = [...prev] + newDataList[index] = {cost: null, latency: null, usage: null} + return newDataList + }) } } finally { setIsRunning((prevState) => { @@ -415,81 +422,34 @@ const App: React.FC = ({ if (abortControllersRef.current[index]) { abortControllersRef.current[index].abort() } + if (compareMode && isRunning[index]) { + const testItem = testList[index] - const testItem = testList[index] - setIsRunning( - (prevState) => { - const newState = [...prevState] - newState[index] = false - return newState - }, - () => { - document - .querySelectorAll(`.testview-cancel-button-${testItem._id}`) - .forEach((btn) => { - if (btn.parentElement?.id !== variant.variantId) { - ;(btn as HTMLButtonElement).click() - } - }) - }, - ) - - setResultForIndex("", index) - setAdditionalDataList((prev) => { - const newDataList = [...prev] - newDataList[index] = {cost: null, latency: null, usage: null} - return newDataList - }) + document.querySelectorAll(`.testview-cancel-button-${testItem._id}`).forEach((btn) => { + if (btn.parentElement?.id !== variant.variantId) { + ;(btn as HTMLButtonElement).click() + } + }) + } } const handleCancelAll = () => { - abortControllersRef.current.forEach((controller, index) => { - if (controller) { - controller.abort() - setIsRunning((prevState) => { - const newState = [...prevState] - newState[index] = false - return newState - }) - - setResultForIndex("", index) - setAdditionalDataList((prev) => { - const newDataList = [...prev] - newDataList[index] = {cost: null, latency: null, usage: null} - return newDataList - }) - } - }) + const funcs: Function[] = [] + rootRef.current + ?.querySelectorAll("[class*=testview-cancel-button-]") + .forEach((btn) => funcs.push(() => (btn as HTMLButtonElement).click())) + batchExecute(funcs) } const handleRunAll = async () => { - if (isRunningAll) { - handleCancelAll() - setIsRunningAll(false) - return - } - - setIsRunningAll(true) - - abortControllersRef.current.forEach((controller) => controller && controller.abort()) - - const newAbortControllers = Array(testList.length) - .fill(undefined) - .map(() => new AbortController()) - abortControllersRef.current = newAbortControllers - const funcs: Function[] = [] rootRef.current ?.querySelectorAll("[data-cy=testview-input-parameters-run-button]") .forEach((btn) => funcs.push(() => (btn as HTMLButtonElement).click())) - try { - await batchExecute(funcs) - } catch (e) { - setIsRunningAll(false) - } finally { - setIsRunningAll(false) - } + setIsRunningAll(true) + await batchExecute(funcs) + setIsRunningAll(false) } const handleAddRow = () => { diff --git a/agenta-web/src/hooks/useStateCallback.ts b/agenta-web/src/hooks/useStateCallback.ts index 3a3c8c6cae..6e34c87d42 100644 --- a/agenta-web/src/hooks/useStateCallback.ts +++ b/agenta-web/src/hooks/useStateCallback.ts @@ -1,4 +1,5 @@ -import {SetStateAction, useCallback, useEffect, useRef, useState} from "react" +import {SetStateAction, useCallback, useRef, useState} from "react" +import {useUpdateEffect} from "usehooks-ts" type Callback = (value?: T) => void export type DispatchWithCallback = (value: T, callback?: Callback) => void @@ -15,7 +16,6 @@ function useStateCallback( const [state, _setState] = useState(initialState) const callbackRef = useRef>() - const isFirstCallbackCall = useRef(true) const setState = useCallback( (setStateAction: SetStateAction, callback?: Callback): void => { @@ -25,11 +25,7 @@ function useStateCallback( [], ) - useEffect(() => { - if (isFirstCallbackCall.current) { - isFirstCallbackCall.current = false - return - } + useUpdateEffect(() => { typeof callbackRef.current === "function" && callbackRef.current(state) }, [state]) diff --git a/agenta-web/src/lib/services/api.ts b/agenta-web/src/lib/services/api.ts index 807be1cf90..3b886211a1 100644 --- a/agenta-web/src/lib/services/api.ts +++ b/agenta-web/src/lib/services/api.ts @@ -85,6 +85,7 @@ export async function callVariant( baseId: string, chatMessages?: ChatMessage[], signal?: AbortSignal, + ignoreAxiosError?: boolean, ) { const isChatVariant = Array.isArray(chatMessages) && chatMessages.length > 0 // Separate input parameters into two dictionaries based on the 'input' property @@ -122,9 +123,14 @@ export async function callVariant( const appContainerURI = await getAppContainerURL(appId, undefined, baseId) - return axios.post(`${appContainerURI}/generate`, requestBody, {signal}).then((res) => { - return res.data - }) + return axios + .post(`${appContainerURI}/generate`, requestBody, { + signal, + _ignoreError: ignoreAxiosError, + } as any) + .then((res) => { + return res.data + }) } /** From e423caeac0dd920ab1ece1d152cd2642d246fd08 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Tue, 16 Jan 2024 16:02:53 +0500 Subject: [PATCH 225/267] reverted changes in dev.Dockerfile --- agenta-web/dev.Dockerfile | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/agenta-web/dev.Dockerfile b/agenta-web/dev.Dockerfile index 91060e8aa6..6f86dbd847 100644 --- a/agenta-web/dev.Dockerfile +++ b/agenta-web/dev.Dockerfile @@ -1,37 +1,37 @@ FROM node:18-alpine -# WORKDIR /app +WORKDIR /app -# # Install dependencies based on the preferred package manager -# COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ -# RUN \ -# if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ -# elif [ -f package-lock.json ]; then npm i; \ -# elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ -# # Allow install without lockfile, so example works even without Node.js installed locally -# else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ -# fi +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm i; \ + elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ + # Allow install without lockfile, so example works even without Node.js installed locally + else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ + fi -# COPY src ./src -# COPY public ./public -# COPY next.config.js . -# COPY tsconfig.json . -# COPY postcss.config.js . -# COPY .env . -# RUN if [ -f .env.local ]; then cp .env.local .; fi -# # used in cloud -# COPY sentry.* . -# # Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry -# # Uncomment the following line to disable telemetry at run time -# # ENV NEXT_TELEMETRY_DISABLED 1 +COPY src ./src +COPY public ./public +COPY next.config.js . +COPY tsconfig.json . +COPY postcss.config.js . +COPY .env . +RUN if [ -f .env.local ]; then cp .env.local .; fi +# used in cloud +COPY sentry.* . +# Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry +# Uncomment the following line to disable telemetry at run time +# ENV NEXT_TELEMETRY_DISABLED 1 -# # Note: Don't expose ports here, Compose will handle that for us +# Note: Don't expose ports here, Compose will handle that for us -# # Start Next.js in development mode based on the preferred package manager -# CMD \ -# if [ -f yarn.lock ]; then yarn dev; \ -# elif [ -f package-lock.json ]; then npm run dev; \ -# elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ -# else yarn dev; \ -# fi +# Start Next.js in development mode based on the preferred package manager +CMD \ + if [ -f yarn.lock ]; then yarn dev; \ + elif [ -f package-lock.json ]; then npm run dev; \ + elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ + else yarn dev; \ + fi From a7cb17590d2c957ff0c7b7f90392dbb23d9da404 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Tue, 16 Jan 2024 15:01:31 +0100 Subject: [PATCH 226/267] sync changes with chat --- agenta-web/src/lib/helpers/evaluate.ts | 32 +++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/agenta-web/src/lib/helpers/evaluate.ts b/agenta-web/src/lib/helpers/evaluate.ts index 31ce851ec6..abfac16f40 100644 --- a/agenta-web/src/lib/helpers/evaluate.ts +++ b/agenta-web/src/lib/helpers/evaluate.ts @@ -69,13 +69,15 @@ export const exportABTestingEvaluationData = ( rows: GenericObject[], ) => { const exportRow = rows.map((data, ix) => { - const inputColumns = data.inputs.reduce( - (columns: any, input: {input_name: string; input_value: string}) => { - columns[`${input.input_name}`] = input.input_value - return columns - }, - {}, - ) + const inputColumns = evaluation.testset.testsetChatColumn + ? {Input: evaluation.testset.csvdata[ix]?.[evaluation.testset.testsetChatColumn]} + : data.inputs.reduce( + (columns: any, input: {input_name: string; input_value: string}) => { + columns[`${input.input_name}`] = input.input_value + return columns + }, + {}, + ) return { ...inputColumns, [`App Variant ${evaluation.variants[0].variantName} Output 0`]: data?.columnData0 @@ -105,13 +107,15 @@ export const exportSingleModelEvaluationData = ( rows: GenericObject[], ) => { const exportRow = rows.map((data, ix) => { - const inputColumns = data.inputs.reduce( - (columns: any, input: {input_name: string; input_value: string}) => { - columns[`${input.input_name}`] = input.input_value - return columns - }, - {}, - ) + const inputColumns = evaluation.testset.testsetChatColumn + ? {Input: evaluation.testset.csvdata[ix]?.[evaluation.testset.testsetChatColumn]} + : data.inputs.reduce( + (columns: any, input: {input_name: string; input_value: string}) => { + columns[`${input.input_name}`] = input.input_value + return columns + }, + {}, + ) const numericScore = parseInt(data.score) return { ...inputColumns, From ce05aa2e8f06e7a7a1a2a19a4a26c3c913c2c1b9 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Tue, 16 Jan 2024 15:14:43 +0100 Subject: [PATCH 227/267] added annotation and note in tab view for human eval --- .../ABTestingEvaluationTable.tsx | 125 +++++++++++++++--- .../SingleModelEvaluationTable.tsx | 110 ++++++++++++--- rabbitmq_data/.erlang.cookie | 1 + 3 files changed, 201 insertions(+), 35 deletions(-) create mode 100644 rabbitmq_data/.erlang.cookie diff --git a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx index 8c4ca6fc27..ecfd1720cb 100644 --- a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx @@ -1,6 +1,18 @@ -import {useState, useEffect} from "react" +import {useState, useEffect, useCallback} from "react" import type {ColumnType} from "antd/es/table" -import {Button, Card, Col, Radio, Row, Space, Statistic, Table, Typography, message} from "antd" +import { + Button, + Card, + Col, + Input, + Radio, + Row, + Space, + Statistic, + Table, + Typography, + message, +} from "antd" import { updateEvaluationScenario, callVariant, @@ -22,6 +34,7 @@ import EvaluationVotePanel from "../Evaluations/EvaluationCardView/EvaluationVot import VariantAlphabet from "../Evaluations/EvaluationCardView/VariantAlphabet" import {ParamsFormWithRun} from "./SingleModelEvaluationTable" import {PassThrough} from "stream" +import {debounce} from "lodash" const {Title} = Typography @@ -90,6 +103,22 @@ const useStyles = createUseStyles({ top: 36, zIndex: 1, }, + sideBar: { + marginTop: "1rem", + display: "flex", + flexDirection: "column", + gap: "2rem", + border: "1px solid #d9d9d9", + borderRadius: 6, + padding: "1rem", + alignSelf: "flex-start", + "&>h4.ant-typography": { + margin: 0, + }, + flex: 0.35, + minWidth: 240, + maxWidth: 500, + }, }) const ABTestingEvaluationTable: React.FC = ({ @@ -117,6 +146,13 @@ const ABTestingEvaluationTable: React.FC = ({ evaluationResults?.votes_data?.variants_votes_data?.[evaluation.variants[1]?.variantId] ?.number_of_votes || 0 + const depouncedUpdateEvaluationScenario = useCallback( + debounce((data: Partial, scenarioId) => { + updateEvaluationScenarioData(scenarioId, data) + }, 800), + [evaluationScenarios], + ) + useEffect(() => { if (evaluationScenarios) { const obj = [...evaluationScenarios] @@ -297,7 +333,7 @@ const ABTestingEvaluationTable: React.FC = ({ ), dataIndex: columnKey, key: columnKey, - width: "25%", + width: "20%", render: (text: any, record: ABTestingEvaluationTableRow, rowIndex: number) => { if (text) return text if (record.outputs && record.outputs.length > 0) { @@ -344,24 +380,81 @@ const ABTestingEvaluationTable: React.FC = ({ ) }, }, + { + key: "correctAnswer", + title: "Expected Output", + dataIndex: "correctAnswer", + width: "25%", + }, ...dynamicColumns, { title: "Evaluate", dataIndex: "evaluate", key: "evaluate", - width: 200, - // fixed: 'right', - render: (text: any, record: any, rowIndex: number) => ( - handleVoteClick(record.id, vote)} - loading={record.vote === "loading"} - vertical - key={record.id} - /> - ), + width: 300, + render: (text: any, record: any, rowIndex: number) => { + return ( + <> +
+ Submit your feedback + + {record.outputs.length > 0 && + record.outputs.every((item: any) => !!item.variant_output) && ( + + + Which response is better? + + { + + handleVoteClick(record.id, vote) + } + loading={record.vote === "loading"} + vertical + key={record.id} + /> + } + + )} + + + Expected Answer + + depouncedUpdateEvaluationScenario( + { + correctAnswer: e.target.value, + }, + record.id, + ) + } + key={record.id} + /> + + + + Additional Notes + + depouncedUpdateEvaluationScenario( + {note: e.target.value}, + record.id, + ) + } + key={record.id} + /> + +
+ + ) + }, }, ] diff --git a/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx index efa7f1efcd..66f6f3b510 100644 --- a/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx @@ -1,4 +1,4 @@ -import {useState, useEffect, useCallback} from "react" +import {useState, useEffect, useCallback, useMemo} from "react" import type {ColumnType} from "antd/es/table" import {CaretRightOutlined} from "@ant-design/icons" import { @@ -6,6 +6,7 @@ import { Card, Col, Form, + Input, Radio, Row, Space, @@ -98,6 +99,22 @@ const useStyles = createUseStyles({ top: 36, zIndex: 1, }, + sideBar: { + marginTop: "1rem", + display: "flex", + flexDirection: "column", + gap: "2rem", + border: "1px solid #d9d9d9", + borderRadius: 6, + padding: "1rem", + alignSelf: "flex-start", + "&>h4.ant-typography": { + margin: 0, + }, + flex: 0.35, + minWidth: 240, + maxWidth: 500, + }, }) export const ParamsFormWithRun = ({ @@ -166,6 +183,13 @@ const SingleModelEvaluationTable: React.FC = ({ const [viewMode, setViewMode] = useQueryParam("viewMode", "card") const [accuracy, setAccuracy] = useState(0) + const depouncedUpdateEvaluationScenario = useCallback( + debounce((data: Partial, scenarioId) => { + updateEvaluationScenarioData(scenarioId, data) + }, 800), + [evaluationScenarios], + ) + useEffect(() => { if (evaluationScenarios) { const obj = [...evaluationScenarios] @@ -407,26 +431,74 @@ const SingleModelEvaluationTable: React.FC = ({ title: "Evaluate", dataIndex: "evaluate", key: "evaluate", - width: 200, - // fixed: 'right', + width: 300, render: (text: any, record: any, rowIndex: number) => { return ( - - depouncedHandleScoreChange(record.id, val[0].score as number) - } - loading={record.score === "loading"} - showVariantName={false} - key={record.id} - /> + <> +
+ Submit your feedback + + {record.outputs.length > 0 && + record.outputs.every((item: any) => !!item.variant_output) && ( + + Rate the response + { + + depouncedHandleScoreChange( + record.id, + val[0].score as number, + ) + } + loading={record.score === "loading"} + showVariantName={false} + key={record.id} + /> + } + + )} + + + Expected Answer + + depouncedUpdateEvaluationScenario( + { + correctAnswer: e.target.value, + }, + record.id, + ) + } + key={record.id} + /> + + + + Additional Notes + + depouncedUpdateEvaluationScenario( + {note: e.target.value}, + record.id, + ) + } + key={record.id} + /> + +
+ ) }, }, diff --git a/rabbitmq_data/.erlang.cookie b/rabbitmq_data/.erlang.cookie new file mode 100644 index 0000000000..d08f24abd6 --- /dev/null +++ b/rabbitmq_data/.erlang.cookie @@ -0,0 +1 @@ +RGJHUIUQCKYDUFEZXFQI \ No newline at end of file From f5f4c020c04fd4e9d910e3d36152931a85cf6862 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Tue, 16 Jan 2024 15:17:14 +0100 Subject: [PATCH 228/267] revert changes --- .../components/EvaluationTable/ABTestingEvaluationTable.tsx | 6 ------ rabbitmq_data/.erlang.cookie | 1 - 2 files changed, 7 deletions(-) delete mode 100644 rabbitmq_data/.erlang.cookie diff --git a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx index ecfd1720cb..c804f5051a 100644 --- a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx @@ -380,12 +380,6 @@ const ABTestingEvaluationTable: React.FC = ({ ) }, }, - { - key: "correctAnswer", - title: "Expected Output", - dataIndex: "correctAnswer", - width: "25%", - }, ...dynamicColumns, { title: "Evaluate", diff --git a/rabbitmq_data/.erlang.cookie b/rabbitmq_data/.erlang.cookie deleted file mode 100644 index d08f24abd6..0000000000 --- a/rabbitmq_data/.erlang.cookie +++ /dev/null @@ -1 +0,0 @@ -RGJHUIUQCKYDUFEZXFQI \ No newline at end of file From ec0f59d10c1d2727e41feba55fcde0923437a0d3 Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 16 Jan 2024 15:23:25 +0100 Subject: [PATCH 229/267] Update - rename AppEnvironmentDB collection name and modified query to list environments --- agenta-backend/agenta_backend/models/db_models.py | 2 +- agenta-backend/agenta_backend/services/db_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agenta-backend/agenta_backend/models/db_models.py b/agenta-backend/agenta_backend/models/db_models.py index 11ad909077..14125caecc 100644 --- a/agenta-backend/agenta_backend/models/db_models.py +++ b/agenta-backend/agenta_backend/models/db_models.py @@ -162,7 +162,7 @@ class AppEnvironmentDB(Document): created_at: Optional[datetime] = Field(default=datetime.utcnow()) class Settings: - name = "app_environment_db" + name = "environments" class TemplateDB(Document): diff --git a/agenta-backend/agenta_backend/services/db_manager.py b/agenta-backend/agenta_backend/services/db_manager.py index adbac48879..c2486c8e4d 100644 --- a/agenta-backend/agenta_backend/services/db_manager.py +++ b/agenta-backend/agenta_backend/services/db_manager.py @@ -1009,7 +1009,7 @@ async def list_environments_by_variant( """ environments_db = await AppEnvironmentDB.find( - AppEnvironmentDB.app == app_variant.app.id, fetch_links=True + AppEnvironmentDB.app.id == app_variant.app.id, fetch_links=True ).to_list() return environments_db From 944d7743a625c8e792e5f2ed23041fa1b5923d70 Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 16 Jan 2024 15:24:59 +0100 Subject: [PATCH 230/267] Update - modified migration 2 and 5 files --- .../migrations/20240110165900_evaluations_revamp.py | 2 ++ .../20240113204909_change_odmantic_reference_to_link.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 689b277865..7e32e32714 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -333,6 +333,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): variant=PydanticObjectId(variant), evaluators_configs=auto_evaluator_configs, aggregated_results=[], + created_at=old_evaluation.created_at, ) await new_eval.insert(session=session) @@ -351,6 +352,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): evaluation_type=old_evaluation.evaluation_type, variants=old_evaluation.variants, testset=old_evaluation.testset, + created_at=old_evaluation.created_at, ) await new_eval.insert(session=session) diff --git a/agenta-backend/agenta_backend/migrations/20240113204909_change_odmantic_reference_to_link.py b/agenta-backend/agenta_backend/migrations/20240113204909_change_odmantic_reference_to_link.py index 12518af42c..4c5f164d29 100644 --- a/agenta-backend/agenta_backend/migrations/20240113204909_change_odmantic_reference_to_link.py +++ b/agenta-backend/agenta_backend/migrations/20240113204909_change_odmantic_reference_to_link.py @@ -164,7 +164,7 @@ class AppEnvironmentDB(Document): created_at: Optional[datetime] = Field(default=datetime.utcnow()) class Settings: - name = "app_environment_db" + name = "environments" class TemplateDB(Document): From 6146555080dbacc616c1541b3496cdec3f2215de Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Tue, 16 Jan 2024 17:23:05 +0100 Subject: [PATCH 231/267] add backup script to save time --- .../agenta_backend/migrations/backup.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 agenta-backend/agenta_backend/migrations/backup.py diff --git a/agenta-backend/agenta_backend/migrations/backup.py b/agenta-backend/agenta_backend/migrations/backup.py new file mode 100644 index 0000000000..ffbbb60212 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/backup.py @@ -0,0 +1,30 @@ +import asyncio +from pymongo import MongoClient + +async def drop_and_restore_collections(session=None): + print("dropping and restoring collections") + client = MongoClient("mongodb://username:password@mongo") + backup_db_name = "agenta_v2_backup" + main_db = "agenta_v2" + agenta_v2_db = client[main_db] + + # Drop all collections in the agenta_v2 database + for collection in agenta_v2_db.list_collection_names(): + agenta_v2_db[collection].drop() + + # Restore collections from agenta_v2_cloud_backup database + backup_db = client[backup_db_name] + for collection in backup_db.list_collection_names(): + data = list(backup_db[collection].find()) + if data: + agenta_v2_db[collection].insert_many(data) + + client.close() + +# Main entry point for the script +async def main(): + await drop_and_restore_collections() + +# Run the main function +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file From 6100cc94e3efb13f211960e4247d0527c6663d92 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 16 Jan 2024 18:18:27 +0100 Subject: [PATCH 232/267] reverted dev.dockerfile --- agenta-web/dev.Dockerfile | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/agenta-web/dev.Dockerfile b/agenta-web/dev.Dockerfile index ad5bc9a53b..6af573852c 100644 --- a/agenta-web/dev.Dockerfile +++ b/agenta-web/dev.Dockerfile @@ -1,37 +1,37 @@ FROM node:18-alpine -# WORKDIR /app +WORKDIR /app -# # Install dependencies based on the preferred package manager -# COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ -# RUN \ -# if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ -# elif [ -f package-lock.json ]; then npm i; \ -# elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ -# # Allow install without lockfile, so example works even without Node.js installed locally -# else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ -# fi +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm i; \ + elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ + # Allow install without lockfile, so example works even without Node.js installed locally + else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ + fi -# COPY src ./src -# COPY public ./public -# COPY next.config.js . -# COPY tsconfig.json . -# COPY postcss.config.js . -# COPY .env . -# RUN if [ -f .env.local ]; then cp .env.local .; fi -# # # used in cloud -# COPY sentry.* . -# # Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry -# # Uncomment the following line to disable telemetry at run time -# # ENV NEXT_TELEMETRY_DISABLED 1 +COPY src ./src +COPY public ./public +COPY next.config.js . +COPY tsconfig.json . +COPY postcss.config.js . +COPY .env . +RUN if [ -f .env.local ]; then cp .env.local .; fi +# # used in cloud +COPY sentry.* . +# Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry +# Uncomment the following line to disable telemetry at run time +# ENV NEXT_TELEMETRY_DISABLED 1 -# # Note: Don't expose ports here, Compose will handle that for us +# Note: Don't expose ports here, Compose will handle that for us -# # Start Next.js in development mode based on the preferred package manager -# CMD \ -# if [ -f yarn.lock ]; then yarn dev; \ -# elif [ -f package-lock.json ]; then npm run dev; \ -# elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ -# else yarn dev; \ -# fi +# Start Next.js in development mode based on the preferred package manager +CMD \ + if [ -f yarn.lock ]; then yarn dev; \ + elif [ -f package-lock.json ]; then npm run dev; \ + elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ + else yarn dev; \ + fi From 556598169f15bf730b866752ae6690a31920bf92 Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 16 Jan 2024 18:28:09 +0100 Subject: [PATCH 233/267] Update - modified logic to allow a/b evaluation to be separate after migration --- .../20240110165900_evaluations_revamp.py | 55 +++++++++++++------ ...40112120721_evaluation_scenarios_revamp.py | 4 +- .../services/results_service.py | 2 +- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 689b277865..c995f2e2e9 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import Any, Dict, List, Optional +from beanie.operators import In from pydantic import BaseModel, Field from beanie import free_fall_migration, Document, Link, PydanticObjectId @@ -191,7 +192,20 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): # Create a key-value store that saves all the variants & evaluation types for a particular app id # Example: {"app_id": {"evaluation_types": ["string", "string"], "variant_ids": ["string", "string"]}} app_keyvalue_store = {} - old_evaluations = await OldEvaluationDB.find(fetch_links=True).to_list() + old_evaluations = await OldEvaluationDB.find( + In( + OldEvaluationDB.evaluation_type, + [ + "auto_exact_match", + "auto_similarity_match", + "auto_regex_test", + "auto_ai_critique", + "auto_custom_code_run", + "auto_webhook_test", + ], + ), + fetch_links=True, + ).to_list() for old_eval in old_evaluations: app_id = old_eval.app.id variant_ids = [str(variant_id) for variant_id in old_eval.variants] @@ -338,21 +352,30 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): # STEP 5: # Create the human evaluation - for old_evaluation in old_evaluations: - if old_evaluation.evaluation_type in [ - "human_a_b_testing", - "single_model_test", - ]: - new_eval = HumanEvaluationDB( - app=old_evaluation.app, - organization=old_evaluation.organization, - user=old_evaluation.user, - status=old_evaluation.status, - evaluation_type=old_evaluation.evaluation_type, - variants=old_evaluation.variants, - testset=old_evaluation.testset, - ) - await new_eval.insert(session=session) + old_human_evaluations = await OldEvaluationDB.find( + In( + OldEvaluationDB.evaluation_type, + [ + "human_a_b_testing", + "single_model_test", + ], + ), + fetch_links=True, + ).to_list() + for old_evaluation in old_human_evaluations: + new_eval = HumanEvaluationDB( + id=old_evaluation.id, + app=old_evaluation.app, + organization=old_evaluation.organization, + user=old_evaluation.user, + status=old_evaluation.status, + evaluation_type=old_evaluation.evaluation_type, + variants=old_evaluation.variants, + testset=old_evaluation.testset, + created_at=old_evaluation.created_at, + updated_at=old_evaluation.updated_at, + ) + await new_eval.insert(session=session) class Backward: diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py index 8ad1f2a105..3af89301cb 100644 --- a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py @@ -372,7 +372,7 @@ async def migrate_old_human_a_b_evaluation_scenario_to_new_human_evaluation_scen ).to_list() for ab_testing_scenario in old_human_ab_testing_scenarios: matching_human_evaluation = await HumanEvaluationDB.find_one( - HumanEvaluationDB.app.id == ab_testing_scenario.evaluation.app.id, + HumanEvaluationDB.id == ab_testing_scenario.evaluation.id, HumanEvaluationDB.evaluation_type == "human_a_b_testing", fetch_links=True, ) @@ -428,7 +428,7 @@ async def migrate_old_human_single_model_evaluation_scenario_to_new_human_evalua ).to_list() for single_model_scenario in old_human_single_model_scenarios: matching_human_evaluation = await HumanEvaluationDB.find_one( - HumanEvaluationDB.app.id == single_model_scenario.evaluation.app.id, + HumanEvaluationDB.id == single_model_scenario.evaluation.id, HumanEvaluationDB.evaluation_type == "single_model_test", fetch_links=True, ) diff --git a/agenta-backend/agenta_backend/services/results_service.py b/agenta-backend/agenta_backend/services/results_service.py index d33a1c419f..d04ee2976e 100644 --- a/agenta-backend/agenta_backend/services/results_service.py +++ b/agenta-backend/agenta_backend/services/results_service.py @@ -11,7 +11,7 @@ async def fetch_results_for_evaluation(evaluation: HumanEvaluationDB): evaluation_scenarios = await HumanEvaluationScenarioDB.find( - HumanEvaluationScenarioDB.evaluation.id == ObjectId(evaluation.id), + HumanEvaluationScenarioDB.evaluation.id == evaluation.id, ).to_list() results = {} From a61d70f37ad3ac3cb3d862324251c8e3d4b8e7df Mon Sep 17 00:00:00 2001 From: Abram Date: Tue, 16 Jan 2024 18:28:09 +0100 Subject: [PATCH 234/267] Update - modified logic to allow a/b evaluation to be separate after migration --- .../20240110165900_evaluations_revamp.py | 55 +++++++++++++------ ...40112120721_evaluation_scenarios_revamp.py | 4 +- .../services/results_service.py | 2 +- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 689b277865..c995f2e2e9 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import Any, Dict, List, Optional +from beanie.operators import In from pydantic import BaseModel, Field from beanie import free_fall_migration, Document, Link, PydanticObjectId @@ -191,7 +192,20 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): # Create a key-value store that saves all the variants & evaluation types for a particular app id # Example: {"app_id": {"evaluation_types": ["string", "string"], "variant_ids": ["string", "string"]}} app_keyvalue_store = {} - old_evaluations = await OldEvaluationDB.find(fetch_links=True).to_list() + old_evaluations = await OldEvaluationDB.find( + In( + OldEvaluationDB.evaluation_type, + [ + "auto_exact_match", + "auto_similarity_match", + "auto_regex_test", + "auto_ai_critique", + "auto_custom_code_run", + "auto_webhook_test", + ], + ), + fetch_links=True, + ).to_list() for old_eval in old_evaluations: app_id = old_eval.app.id variant_ids = [str(variant_id) for variant_id in old_eval.variants] @@ -338,21 +352,30 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): # STEP 5: # Create the human evaluation - for old_evaluation in old_evaluations: - if old_evaluation.evaluation_type in [ - "human_a_b_testing", - "single_model_test", - ]: - new_eval = HumanEvaluationDB( - app=old_evaluation.app, - organization=old_evaluation.organization, - user=old_evaluation.user, - status=old_evaluation.status, - evaluation_type=old_evaluation.evaluation_type, - variants=old_evaluation.variants, - testset=old_evaluation.testset, - ) - await new_eval.insert(session=session) + old_human_evaluations = await OldEvaluationDB.find( + In( + OldEvaluationDB.evaluation_type, + [ + "human_a_b_testing", + "single_model_test", + ], + ), + fetch_links=True, + ).to_list() + for old_evaluation in old_human_evaluations: + new_eval = HumanEvaluationDB( + id=old_evaluation.id, + app=old_evaluation.app, + organization=old_evaluation.organization, + user=old_evaluation.user, + status=old_evaluation.status, + evaluation_type=old_evaluation.evaluation_type, + variants=old_evaluation.variants, + testset=old_evaluation.testset, + created_at=old_evaluation.created_at, + updated_at=old_evaluation.updated_at, + ) + await new_eval.insert(session=session) class Backward: diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py index 8ad1f2a105..3af89301cb 100644 --- a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py @@ -372,7 +372,7 @@ async def migrate_old_human_a_b_evaluation_scenario_to_new_human_evaluation_scen ).to_list() for ab_testing_scenario in old_human_ab_testing_scenarios: matching_human_evaluation = await HumanEvaluationDB.find_one( - HumanEvaluationDB.app.id == ab_testing_scenario.evaluation.app.id, + HumanEvaluationDB.id == ab_testing_scenario.evaluation.id, HumanEvaluationDB.evaluation_type == "human_a_b_testing", fetch_links=True, ) @@ -428,7 +428,7 @@ async def migrate_old_human_single_model_evaluation_scenario_to_new_human_evalua ).to_list() for single_model_scenario in old_human_single_model_scenarios: matching_human_evaluation = await HumanEvaluationDB.find_one( - HumanEvaluationDB.app.id == single_model_scenario.evaluation.app.id, + HumanEvaluationDB.id == single_model_scenario.evaluation.id, HumanEvaluationDB.evaluation_type == "single_model_test", fetch_links=True, ) diff --git a/agenta-backend/agenta_backend/services/results_service.py b/agenta-backend/agenta_backend/services/results_service.py index d33a1c419f..d04ee2976e 100644 --- a/agenta-backend/agenta_backend/services/results_service.py +++ b/agenta-backend/agenta_backend/services/results_service.py @@ -11,7 +11,7 @@ async def fetch_results_for_evaluation(evaluation: HumanEvaluationDB): evaluation_scenarios = await HumanEvaluationScenarioDB.find( - HumanEvaluationScenarioDB.evaluation.id == ObjectId(evaluation.id), + HumanEvaluationScenarioDB.evaluation.id == evaluation.id, ).to_list() results = {} From 25218d23ba544015484c80b6c7c6d53333343dad Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Tue, 16 Jan 2024 21:58:50 +0100 Subject: [PATCH 235/267] modified table to add score, expected answer and notes --- .../ABTestingEvaluationTable.tsx | 121 ++++++++-------- .../SingleModelEvaluationTable.tsx | 134 +++++++++--------- 2 files changed, 127 insertions(+), 128 deletions(-) diff --git a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx index c804f5051a..5162b89314 100644 --- a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx @@ -382,70 +382,69 @@ const ABTestingEvaluationTable: React.FC = ({ }, ...dynamicColumns, { - title: "Evaluate", - dataIndex: "evaluate", - key: "evaluate", - width: 300, + title: "Score", + dataIndex: "score", + key: "score", render: (text: any, record: any, rowIndex: number) => { return ( <> -
- Submit your feedback - - {record.outputs.length > 0 && - record.outputs.every((item: any) => !!item.variant_output) && ( - - - Which response is better? - - { - - handleVoteClick(record.id, vote) - } - loading={record.vote === "loading"} - vertical - key={record.id} - /> - } - - )} - - - Expected Answer - - depouncedUpdateEvaluationScenario( - { - correctAnswer: e.target.value, - }, - record.id, - ) - } - key={record.id} - /> - - - - Additional Notes - - depouncedUpdateEvaluationScenario( - {note: e.target.value}, - record.id, - ) - } - key={record.id} - /> - -
+ { + handleVoteClick(record.id, vote)} + loading={record.vote === "loading"} + vertical + key={record.id} + /> + } + + ) + }, + }, + { + title: "Expected Answer", + dataIndex: "expectedAnswer", + key: "expectedAnswer", + render: (text: any, record: any, rowIndex: number) => { + let correctAnswer = + record.correctAnswer || evaluation.testset.csvdata[rowIndex].correct_answer + + return ( + <> + + depouncedUpdateEvaluationScenario( + { + correctAnswer: e.target.value, + }, + record.id, + ) + } + key={record.id} + /> + + ) + }, + }, + { + title: "Additional Note", + dataIndex: "additionalNote", + key: "additionalNote", + render: (text: any, record: any, rowIndex: number) => { + return ( + <> + + depouncedUpdateEvaluationScenario({note: e.target.value}, record.id) + } + key={record.id} + /> ) }, diff --git a/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx index 66f6f3b510..d89865bcb5 100644 --- a/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx @@ -428,76 +428,76 @@ const SingleModelEvaluationTable: React.FC = ({ }, ...dynamicColumns, { - title: "Evaluate", - dataIndex: "evaluate", - key: "evaluate", - width: 300, + title: "Score", + dataIndex: "score", + key: "score", render: (text: any, record: any, rowIndex: number) => { return ( <> -
- Submit your feedback - - {record.outputs.length > 0 && - record.outputs.every((item: any) => !!item.variant_output) && ( - - Rate the response - { - - depouncedHandleScoreChange( - record.id, - val[0].score as number, - ) - } - loading={record.score === "loading"} - showVariantName={false} - key={record.id} - /> - } - - )} - - - Expected Answer - - depouncedUpdateEvaluationScenario( - { - correctAnswer: e.target.value, - }, - record.id, - ) - } - key={record.id} - /> - - - - Additional Notes - - depouncedUpdateEvaluationScenario( - {note: e.target.value}, - record.id, - ) - } - key={record.id} - /> - -
+ { + + depouncedHandleScoreChange(record.id, val[0].score as number) + } + loading={record.score === "loading"} + showVariantName={false} + key={record.id} + /> + } + + ) + }, + }, + { + title: "Expected Answer", + dataIndex: "expectedAnswer", + key: "expectedAnswer", + render: (text: any, record: any, rowIndex: number) => { + let correctAnswer = + record.correctAnswer || evaluation.testset.csvdata[rowIndex].correct_answer + + return ( + <> + + depouncedUpdateEvaluationScenario( + { + correctAnswer: e.target.value, + }, + record.id, + ) + } + key={record.id} + /> + + ) + }, + }, + { + title: "Additional Note", + dataIndex: "additionalNote", + key: "additionalNote", + render: (text: any, record: any, rowIndex: number) => { + return ( + <> + + depouncedUpdateEvaluationScenario({note: e.target.value}, record.id) + } + key={record.id} + /> ) }, From 4e5e85f88f09d68210ab0836505a5a79421463e1 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 16 Jan 2024 22:20:13 +0100 Subject: [PATCH 236/267] bug fix in deletion of environments --- agenta-backend/agenta_backend/services/db_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/services/db_manager.py b/agenta-backend/agenta_backend/services/db_manager.py index adbac48879..66b51b44fd 100644 --- a/agenta-backend/agenta_backend/services/db_manager.py +++ b/agenta-backend/agenta_backend/services/db_manager.py @@ -881,7 +881,7 @@ async def remove_app_variant_from_db(app_variant_db: AppVariantDB, **kwargs: dic ) for environment in environments: environment.deployed_app_variant = None - await environment.create() + await environment.save() # removing the config config = app_variant_db.config await config.delete() From a227847d9ce318e1574578ff7c3376ec2e7a5a77 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Tue, 16 Jan 2024 22:20:34 +0100 Subject: [PATCH 237/267] format --- agenta-backend/agenta_backend/migrations/backup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/migrations/backup.py b/agenta-backend/agenta_backend/migrations/backup.py index ffbbb60212..9e01bce284 100644 --- a/agenta-backend/agenta_backend/migrations/backup.py +++ b/agenta-backend/agenta_backend/migrations/backup.py @@ -1,6 +1,7 @@ import asyncio from pymongo import MongoClient + async def drop_and_restore_collections(session=None): print("dropping and restoring collections") client = MongoClient("mongodb://username:password@mongo") @@ -21,10 +22,12 @@ async def drop_and_restore_collections(session=None): client.close() + # Main entry point for the script async def main(): await drop_and_restore_collections() + # Run the main function if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From eceec6e96b598ab829149337516f16abeccf8b71 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 17 Jan 2024 08:10:34 +0100 Subject: [PATCH 238/267] Refactor - simplified logic to evaluation migration --- .../20240110165900_evaluations_revamp.py | 289 ++++++++---------- 1 file changed, 124 insertions(+), 165 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 6b65cb79ae..f324499837 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -151,26 +151,7 @@ class Settings: name = "custom_evaluations" -def modify_app_id_store( - app_id: str, - variant_ids: str, - evaluation_type: str, - app_keyvalue_store: Dict[str, Dict[str, List[str]]], -): - app_id_store = app_keyvalue_store.get(app_id, None) - if not app_id_store: - app_keyvalue_store[app_id] = {"variant_ids": [], "evaluation_types": []} - app_id_store = app_keyvalue_store[app_id] - - app_id_store_variant_ids = list(app_id_store["variant_ids"]) - if variant_ids not in app_id_store_variant_ids: - app_id_store_variant_ids.extend(variant_ids) - app_id_store["variant_ids"] = list(set(app_id_store_variant_ids)) - - app_id_store_evaluation_types = list(app_id_store["evaluation_types"]) - if evaluation_type not in app_id_store_evaluation_types: - app_id_store_evaluation_types.append(evaluation_type) - app_id_store["evaluation_types"] = list(set(app_id_store_evaluation_types)) +PYTHON_CODE = "import random from typing import Dict def evaluate( app_params: Dict[str, str], inputs: Dict[str, str], output: str, correct_answer: str ) -> float: return random.uniform(0.1, 0.9)" class Forward: @@ -189,9 +170,31 @@ class Forward: ) async def migrate_old_evaluation_to_new_evaluation(self, session): # STEP 1: - # Create a key-value store that saves all the variants & evaluation types for a particular app id - # Example: {"app_id": {"evaluation_types": ["string", "string"], "variant_ids": ["string", "string"]}} - app_keyvalue_store = {} + # Retrieve all the apps. + # Generate an "exact_match" evaluator and a code evaluator for each app. + apps_db = await AppDB.find(fetch_links=True).to_list() + for app_db in apps_db: + eval_exact_match_config = EvaluatorConfigDB( + app=app_db, + organization=app_db.organization, + user=app_db.user, + name=f"{app_db.app_name}_exact_match_default", + evaluator_key="auto_exact_match", + settings_values={}, + ) + await eval_exact_match_config.insert(session=session) + eval_custom_code_config = EvaluatorConfigDB( + app=app_db, + organization=app_db.organization, + user=app_db.user, + name=f"{app_db.app_name}_custom_code_default", + evaluator_key="auto_custom_code_run", + settings_values=dict({"code": PYTHON_CODE}), + ) + await eval_custom_code_config.insert(session=session) + + # STEP 2: + # Review the evaluations and create a unique evaluation for each one. old_evaluations = await OldEvaluationDB.find( In( OldEvaluationDB.evaluation_type, @@ -207,151 +210,107 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): fetch_links=True, ).to_list() for old_eval in old_evaluations: - app_id = old_eval.app.id - variant_ids = [str(variant_id) for variant_id in old_eval.variants] + list_of_eval_configs = [] evaluation_type = old_eval.evaluation_type - modify_app_id_store( - str(app_id), variant_ids, evaluation_type, app_keyvalue_store + # Use the created evaluator if the evaluation uses "exact_match" or a code evaluator. + # Otherwise, create a new evaluator. + if evaluation_type == "custom_code_run": + eval_config = await EvaluatorConfigDB.find_one( + EvaluatorConfigDB.app.id == old_eval.app.id, + EvaluatorConfigDB.evaluator_key == "auto_custom_code_run", + ) + list_of_eval_configs.append(eval_config.id) + + if evaluation_type == "auto_exact_match": + eval_config = await EvaluatorConfigDB.find_one( + EvaluatorConfigDB.app.id == old_eval.app.id, + EvaluatorConfigDB.evaluator_key == "auto_exact_match", + ) + list_of_eval_configs.append(eval_config.id) + + if evaluation_type == "auto_similarity_match": + eval_config = EvaluatorConfigDB( + app=old_eval.app, + organization=old_eval.organization, + user=old_eval.user, + name=f"{old_eval.app.app_name}_{evaluation_type}", + evaluator_key=evaluation_type, + settings_values=dict( + { + "similarity_threshold": float( + old_eval.evaluation_type_settings.similarity_threshold + ) + } + ), + ) + await eval_config.insert(session=session) + list_of_eval_configs.append(eval_config.id) + + if evaluation_type == "auto_regex_test": + eval_config = EvaluatorConfigDB( + app=old_eval.app, + organization=old_eval.organization, + user=old_eval.user, + name=f"{old_eval.app.app_name}_{evaluation_type}", + evaluator_key=evaluation_type, + settings_values=dict( + { + "regex_pattern": old_eval.evaluation_type_settings.regex_pattern, + "regex_should_match": old_eval.evaluation_type_settings.regex_should_match, + } + ), + ) + await eval_config.insert(session=session) + list_of_eval_configs.append(eval_config.id) + + if evaluation_type == "auto_webhook_test": + eval_config = EvaluatorConfigDB( + app=old_eval.app, + organization=old_eval.organization, + user=old_eval.user, + name=f"{old_eval.app.app_name}_{evaluation_type}", + evaluator_key=evaluation_type, + settings_values=dict( + { + "webhook_url": old_eval.evaluation_type_settings.webhook_url, + "webhook_body": {}, + } + ), + ) + await eval_config.insert(session=session) + list_of_eval_configs.append(eval_config) + + if evaluation_type == "auto_ai_critique": + eval_config = EvaluatorConfigDB( + app=old_eval.app, + organization=old_eval.organization, + user=old_eval.user, + name=f"{old_eval.app.app_name}_{evaluation_type}", + evaluator_key=evaluation_type, + settings_values=dict( + { + "prompt_template": old_eval.evaluation_type_settings.evaluation_prompt_template + } + ), + ) + await eval_config.insert(session=session) + list_of_eval_configs.append(eval_config) + + new_eval = EvaluationDB( + id=old_eval.id, + app=old_eval.app, + organization=old_eval.organization, + user=old_eval.user, + status=old_eval.status, + testset=old_eval.testset, + variant=PydanticObjectId(old_eval.variants[0]), + evaluators_configs=list_of_eval_configs, + aggregated_results=[], + created_at=old_eval.created_at, ) + await new_eval.insert(session=session) - # STEP 2: - # Loop through the app_id key-store to create evaluator configs - # based on the evaluation types available - for app_id, app_id_store in app_keyvalue_store.items(): - app_evaluator_configs: List[EvaluatorConfigDB] = [] - app_db = await AppDB.find_one(AppDB.id == PydanticObjectId(app_id)) - for evaluation_type in app_id_store[ - "evaluation_types" - ]: # the values in this case are the evaluation type - custom_code_evaluations = await OldCustomEvaluationDB.find( - OldCustomEvaluationDB.app == PydanticObjectId(app_id) - ).to_list() - if evaluation_type == "custom_code_run": - for custom_code_evaluation in custom_code_evaluations: - eval_config = EvaluatorConfigDB( - app=app_db, - organization=app_db.organization, - user=app_db.user, - name=f"{app_db.app_name}_{evaluation_type}", - evaluator_key=f"auto_{evaluation_type}", - settings_values=dict( - {"code": custom_code_evaluation.python_code} - ), - ) - await eval_config.insert(session=session) - app_evaluator_configs.append(eval_config) - - if evaluation_type == "auto_similarity_match": - eval_config = EvaluatorConfigDB( - app=app_db, - organization=app_db.organization, - user=app_db.user, - name=f"{app_db.app_name}_{evaluation_type}", - evaluator_key=evaluation_type, - settings_values=dict( - { - "similarity_threshold": float( - old_eval.evaluation_type_settings.similarity_threshold - ) - } - ), - ) - await eval_config.insert(session=session) - app_evaluator_configs.append(eval_config) - - if evaluation_type == "auto_exact_match": - eval_config = EvaluatorConfigDB( - app=app_db, - organization=app_db.organization, - user=app_db.user, - name=f"{app_db.app_name}_{evaluation_type}", - evaluator_key=evaluation_type, - settings_values={}, - ) - await eval_config.insert(session=session) - app_evaluator_configs.append(eval_config) - - if evaluation_type == "auto_regex_test": - eval_config = EvaluatorConfigDB( - app=app_db, - organization=app_db.organization, - user=app_db.user, - name=f"{app_db.app_name}_{evaluation_type}", - evaluator_key=evaluation_type, - settings_values=dict( - { - "regex_pattern": old_eval.evaluation_type_settings.regex_pattern, - "regex_should_match": old_eval.evaluation_type_settings.regex_should_match, - } - ), - ) - await eval_config.insert(session=session) - app_evaluator_configs.append(eval_config) - - if evaluation_type == "auto_webhook_test": - eval_config = EvaluatorConfigDB( - app=app_db, - organization=app_db.organization, - user=app_db.user, - name=f"{app_db.app_name}_{evaluation_type}", - evaluator_key=evaluation_type, - settings_values=dict( - { - "webhook_url": old_eval.evaluation_type_settings.webhook_url, - "webhook_body": {}, - } - ), - ) - await eval_config.insert(session=session) - app_evaluator_configs.append(eval_config) - - if evaluation_type == "auto_ai_critique": - eval_config = EvaluatorConfigDB( - app=app_db, - organization=app_db.organization, - user=app_db.user, - name=f"{app_db.app_name}_{evaluation_type}", - evaluator_key=evaluation_type, - settings_values=dict( - { - "prompt_template": old_eval.evaluation_type_settings.evaluation_prompt_template - } - ), - ) - await eval_config.insert(session=session) - app_evaluator_configs.append(eval_config) - - # STEP 3: - # Retrieve evaluator configs for app id - auto_evaluator_configs: List[PydanticObjectId] = [] - for evaluator_config in app_evaluator_configs: - # In the case where the evaluator key is not a human evaluator, - # Append the evaluator config id in the list of auto evaluator configs - if evaluator_config.evaluator_key not in [ - "human_a_b_testing", - "single_model_test", - ]: - auto_evaluator_configs.append(evaluator_config.id) - - # STEP 4: - # Proceed to create a single evaluation for every variant in the app_id_store - # with the auto_evaluator_configs - if auto_evaluator_configs is not None: - for variant in app_id_store["variant_ids"]: - new_eval = EvaluationDB( - app=app_db, - organization=app_db.organization, - user=app_db.user, - status=old_eval.status, - testset=old_eval.testset, - variant=PydanticObjectId(variant), - evaluators_configs=auto_evaluator_configs, - aggregated_results=[], - created_at=old_evaluation.created_at, - ) - await new_eval.insert(session=session) - - # STEP 5: + # STEP 3: # Create the human evaluation old_human_evaluations = await OldEvaluationDB.find( In( From 0c850cce6cd24d0e26cbff3a155d69c01a47e890 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 17 Jan 2024 08:34:39 +0100 Subject: [PATCH 239/267] Refactor - simplify logic for evaluation scenarios --- .../20240110165900_evaluations_revamp.py | 14 +- ...40112120721_evaluation_scenarios_revamp.py | 138 +++++++----------- .../agenta_backend/migrations/backup.py | 5 +- 3 files changed, 63 insertions(+), 94 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index f324499837..51dca70eaa 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -151,7 +151,7 @@ class Settings: name = "custom_evaluations" -PYTHON_CODE = "import random from typing import Dict def evaluate( app_params: Dict[str, str], inputs: Dict[str, str], output: str, correct_answer: str ) -> float: return random.uniform(0.1, 0.9)" +PYTHON_CODE = "import random \nfrom typing import Dict \n\n\ndef evaluate(\n app_params: Dict[str, str], \n inputs: Dict[str, str], \n output: str, correct_answer: str \n) -> float: \n return random.uniform(0.1, 0.9)" class Forward: @@ -203,7 +203,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): "auto_similarity_match", "auto_regex_test", "auto_ai_critique", - "auto_custom_code_run", + "custom_code_run", "auto_webhook_test", ], ), @@ -219,14 +219,16 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): EvaluatorConfigDB.app.id == old_eval.app.id, EvaluatorConfigDB.evaluator_key == "auto_custom_code_run", ) - list_of_eval_configs.append(eval_config.id) + if eval_config is not None: + list_of_eval_configs.append(eval_config.id) if evaluation_type == "auto_exact_match": eval_config = await EvaluatorConfigDB.find_one( EvaluatorConfigDB.app.id == old_eval.app.id, EvaluatorConfigDB.evaluator_key == "auto_exact_match", ) - list_of_eval_configs.append(eval_config.id) + if eval_config is not None: + list_of_eval_configs.append(eval_config.id) if evaluation_type == "auto_similarity_match": eval_config = EvaluatorConfigDB( @@ -278,7 +280,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): ), ) await eval_config.insert(session=session) - list_of_eval_configs.append(eval_config) + list_of_eval_configs.append(eval_config.id) if evaluation_type == "auto_ai_critique": eval_config = EvaluatorConfigDB( @@ -294,7 +296,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): ), ) await eval_config.insert(session=session) - list_of_eval_configs.append(eval_config) + list_of_eval_configs.append(eval_config.id) new_eval = EvaluationDB( id=old_eval.id, diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py index 3af89301cb..b34fd5c5c1 100644 --- a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py @@ -229,42 +229,6 @@ class Settings: name = "evaluation_scenarios" -class OldCustomEvaluationDB(Document): - evaluation_name: str - python_code: str - version: str = Field("odmantic") - app: Link[AppDB] - user: Link[UserDB] - organization: Link[OrganizationDB] - created_at: Optional[datetime] = Field(default=datetime.utcnow()) - updated_at: Optional[datetime] = Field(default=datetime.utcnow()) - - class Settings: - name = "custom_evaluations" - - -def modify_app_id_store( - app_id: str, - variant_ids: str, - evaluation_type: str, - app_keyvalue_store: Dict[str, Dict[str, List[str]]], -): - app_id_store = app_keyvalue_store.get(app_id, None) - if not app_id_store: - app_keyvalue_store[app_id] = {"variant_ids": [], "evaluation_types": []} - app_id_store = app_keyvalue_store[app_id] - - app_id_store_variant_ids = list(app_id_store["variant_ids"]) - if variant_ids not in list(app_id_store["variant_ids"]): - app_id_store_variant_ids.extend(variant_ids) - app_id_store["variant_ids"] = list(set(app_id_store_variant_ids)) - - app_id_store_evaluation_types = list(app_id_store["evaluation_types"]) - if evaluation_type not in app_id_store_evaluation_types: - app_id_store_evaluation_types.append(evaluation_type) - app_id_store["evaluation_types"] = list(set(app_id_store_evaluation_types)) - - class Forward: @free_fall_migration( document_models=[ @@ -283,6 +247,9 @@ class Forward: async def migrate_old_auto_evaluation_scenario_to_new_auto_evaluation_scenario( self, session ): + new_evaluations = await EvaluationDB.find( + fetch_links=True, + ).to_list() old_auto_scenarios = await OldEvaluationScenarioDB.find( In( OldEvaluationScenarioDB.evaluation.evaluation_type, @@ -291,63 +258,60 @@ async def migrate_old_auto_evaluation_scenario_to_new_auto_evaluation_scenario( "auto_similarity_match", "auto_regex_test", "auto_ai_critique", - "auto_custom_code_run", + "custom_code_run", "auto_webhook_test", ], ), fetch_links=True, ).to_list() - for old_scenario in old_auto_scenarios: - matching_evaluation = await EvaluationDB.find_one( - EvaluationDB.app.id == old_scenario.evaluation.app.id, - fetch_links=True, - ) - if matching_evaluation: - results = [ - EvaluationScenarioResult( - evaluator_config=PydanticObjectId(evaluator_config), - result=Result( - type="number" - if isinstance(old_scenario.score, int) - else "number" - if isinstance(old_scenario.score, float) - else "string" - if isinstance(old_scenario.score, str) - else "boolean" - if isinstance(old_scenario.score, bool) - else "any", - value=old_scenario.score, - ), - ) - for evaluator_config in matching_evaluation.evaluators_configs - ] - new_scenario = EvaluationScenarioDB( - user=matching_evaluation.user, - organization=matching_evaluation.organization, - evaluation=matching_evaluation, - variant_id=old_scenario.evaluation.variants[0], - inputs=[ - EvaluationScenarioInputDB( - name=input.input_name, - type=type(input.input_value).__name__, - value=input.input_value, - ) - for input in old_scenario.inputs - ], - outputs=[ - EvaluationScenarioOutputDB( - type=type(output.variant_output).__name__, - value=output.variant_output, + for new_evaluation in new_evaluations: + for old_scenario in old_auto_scenarios: + if new_evaluation.id == old_scenario.evaluation.id: + results = [ + EvaluationScenarioResult( + evaluator_config=PydanticObjectId(evaluator_config), + result=Result( + type="number" + if isinstance(old_scenario.score, int) + else "number" + if isinstance(old_scenario.score, float) + else "string" + if isinstance(old_scenario.score, str) + else "boolean" + if isinstance(old_scenario.score, bool) + else "any", + value=old_scenario.score, + ), ) - for output in old_scenario.outputs - ], - correct_answer=old_scenario.correct_answer, - is_pinned=old_scenario.is_pinned, - note=old_scenario.note, - evaluators_configs=matching_evaluation.evaluators_configs, - results=results, - ) - await new_scenario.insert(session=session) + for evaluator_config in new_evaluation.evaluators_configs + ] + new_scenario = EvaluationScenarioDB( + user=new_evaluation.user, + organization=new_evaluation.organization, + evaluation=new_evaluation, + variant_id=old_scenario.evaluation.variants[0], + inputs=[ + EvaluationScenarioInputDB( + name=input.input_name, + type=type(input.input_value).__name__, + value=input.input_value, + ) + for input in old_scenario.inputs + ], + outputs=[ + EvaluationScenarioOutputDB( + type=type(output.variant_output).__name__, + value=output.variant_output, + ) + for output in old_scenario.outputs + ], + correct_answer=old_scenario.correct_answer, + is_pinned=old_scenario.is_pinned, + note=old_scenario.note, + evaluators_configs=new_evaluation.evaluators_configs, + results=results, + ) + await new_scenario.insert(session=session) @free_fall_migration( document_models=[ diff --git a/agenta-backend/agenta_backend/migrations/backup.py b/agenta-backend/agenta_backend/migrations/backup.py index ffbbb60212..9e01bce284 100644 --- a/agenta-backend/agenta_backend/migrations/backup.py +++ b/agenta-backend/agenta_backend/migrations/backup.py @@ -1,6 +1,7 @@ import asyncio from pymongo import MongoClient + async def drop_and_restore_collections(session=None): print("dropping and restoring collections") client = MongoClient("mongodb://username:password@mongo") @@ -21,10 +22,12 @@ async def drop_and_restore_collections(session=None): client.close() + # Main entry point for the script async def main(): await drop_and_restore_collections() + # Run the main function if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From 14fe29024c962ae9a750c1f2f1a4f1013fc29811 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 17 Jan 2024 08:55:27 +0100 Subject: [PATCH 240/267] Update - put a 2 seconds sleept --- .../20240113131802_new_evaluation_results_aggregation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py index 09856eda80..2f5a8c4c2a 100644 --- a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py +++ b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py @@ -1,3 +1,4 @@ +import asyncio from datetime import datetime from typing import Any, Dict, List, Optional @@ -229,6 +230,9 @@ async def aggregate_new_evaluation_with_evaluation_scenario_results(self, sessio evaluation_keyvalue_store, ) + print("EKVS: ", evaluation_keyvalue_store) + await asyncio.sleep(2) + # STEP 2: # Update the evaluation key-value store new_auto_evaluation_scenarios = await EvaluationScenarioDB.find( From cf41da4142dec692e93b42818f1d93f94a4a2585 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 17 Jan 2024 09:30:28 +0100 Subject: [PATCH 241/267] Update - update results aggregation logic --- ...1802_new_evaluation_results_aggregation.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py index 2f5a8c4c2a..0a9880a5c4 100644 --- a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py +++ b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py @@ -219,7 +219,7 @@ class Forward: async def aggregate_new_evaluation_with_evaluation_scenario_results(self, session): # STEP 1: # Create a key-value store that saves all the evaluator configs & results for a particular evaluation id - # Example: {"evaluation_id": {"evaluation_config_id": {"results": [Result("type": str, "value": Any)]}}} + # Example: {"evaluation_id": {"evaluation_config_id": {"results": [}}} evaluation_keyvalue_store = {} new_auto_evaluations = await EvaluationDB.find().to_list() for auto_evaluation in new_auto_evaluations: @@ -240,13 +240,20 @@ async def aggregate_new_evaluation_with_evaluation_scenario_results(self, sessio ).to_list() for auto_evaluation in new_auto_evaluation_scenarios: evaluation_id = str(auto_evaluation.evaluation.id) - evaluation_store = evaluation_keyvalue_store[evaluation_id] - configs_with_results = zip( - auto_evaluation.evaluators_configs, auto_evaluation.results - ) - for evaluator, result in configs_with_results: - modify_evaluation_scenario_store( - str(evaluator), result, evaluation_store + + # Check if the evaluation_id exists in the key-value store + if evaluation_id in evaluation_keyvalue_store: + evaluation_store = evaluation_keyvalue_store[evaluation_id] + configs_with_results = zip( + auto_evaluation.evaluators_configs, auto_evaluation.results + ) + for evaluator, result in configs_with_results: + modify_evaluation_scenario_store( + str(evaluator), result, evaluation_store + ) + else: + print( + f"Warning: Evaluation ID {evaluation_id} not found in the key-value store." ) # STEP 3: From 94058607f5ff878f8a9ea5d442bab20f3e18ef6d Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Wed, 17 Jan 2024 10:35:28 +0100 Subject: [PATCH 242/267] fix aggregations --- ...1802_new_evaluation_results_aggregation.py | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py index 0a9880a5c4..2eb249ab93 100644 --- a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py +++ b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py @@ -117,14 +117,13 @@ class Settings: def prepare_evaluation_keyvalue_store( - evaluation_id: str, evaluator_id: str, evaluation_keyvalue_store: Dict + evaluation_id: str, evaluation_keyvalue_store: Dict ) -> Dict[str, Dict[str, Any]]: """ Construct a key-value store to saves results based on a evaluator config in an evaluation Args: evaluation_id (str): ID of evaluation - evaluator_id (str): ID of evaluator config evaluation_keyvalue_store (Dict): evaluation keyvalue store Returns: @@ -134,6 +133,24 @@ def prepare_evaluation_keyvalue_store( if evaluation_id not in evaluation_keyvalue_store: evaluation_keyvalue_store[evaluation_id] = {} + return evaluation_keyvalue_store + + +def prepare_evaluator_keyvalue_store( + evaluation_id: str, evaluator_id: str, evaluation_keyvalue_store: Dict +) -> Dict[str, Dict[str, Any]]: + """ + Construct a key-value store to saves results based on a evaluator config in an evaluation + + Args: + evaluation_id (str): ID of evaluation + evaluator_id (str): ID of evaluator config + evaluation_keyvalue_store (Dict): evaluation keyvalue store + + Returns: + Dict[str, Dict[str, Any]]: {"evaluation_id": {"evaluation_config_id": {"results": [Result("type": str, "value": Any)]}}} + """ + if evaluator_id not in evaluation_keyvalue_store[evaluation_id]: evaluation_keyvalue_store[evaluation_id][evaluator_id] = {"results": []} @@ -222,14 +239,21 @@ async def aggregate_new_evaluation_with_evaluation_scenario_results(self, sessio # Example: {"evaluation_id": {"evaluation_config_id": {"results": [}}} evaluation_keyvalue_store = {} new_auto_evaluations = await EvaluationDB.find().to_list() + print("### len new_auto_evaluations", len(new_auto_evaluations)) + for auto_evaluation in new_auto_evaluations: + evaluation_keyvalue_store = prepare_evaluation_keyvalue_store( + str(auto_evaluation.id), + evaluation_keyvalue_store, + ) for evaluator_config in auto_evaluation.evaluators_configs: - evaluation_keyvalue_store = prepare_evaluation_keyvalue_store( + evaluation_keyvalue_store = prepare_evaluator_keyvalue_store( str(auto_evaluation.id), str(evaluator_config), evaluation_keyvalue_store, ) + print("### len evaluation_keyvalue_store", len(evaluation_keyvalue_store)) print("EKVS: ", evaluation_keyvalue_store) await asyncio.sleep(2) From e3318b44d73663fdf587a7537d1e004d6789b2da Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 17 Jan 2024 10:41:46 +0100 Subject: [PATCH 243/267] Update - modified evaluations revamp migration --- .../20240110165900_evaluations_revamp.py | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 51dca70eaa..f370cf6ad8 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -151,9 +151,6 @@ class Settings: name = "custom_evaluations" -PYTHON_CODE = "import random \nfrom typing import Dict \n\n\ndef evaluate(\n app_params: Dict[str, str], \n inputs: Dict[str, str], \n output: str, correct_answer: str \n) -> float: \n return random.uniform(0.1, 0.9)" - - class Forward: @free_fall_migration( document_models=[ @@ -183,15 +180,6 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): settings_values={}, ) await eval_exact_match_config.insert(session=session) - eval_custom_code_config = EvaluatorConfigDB( - app=app_db, - organization=app_db.organization, - user=app_db.user, - name=f"{app_db.app_name}_custom_code_default", - evaluator_key="auto_custom_code_run", - settings_values=dict({"code": PYTHON_CODE}), - ) - await eval_custom_code_config.insert(session=session) # STEP 2: # Review the evaluations and create a unique evaluation for each one. @@ -215,12 +203,22 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): # Use the created evaluator if the evaluation uses "exact_match" or a code evaluator. # Otherwise, create a new evaluator. if evaluation_type == "custom_code_run": - eval_config = await EvaluatorConfigDB.find_one( - EvaluatorConfigDB.app.id == old_eval.app.id, - EvaluatorConfigDB.evaluator_key == "auto_custom_code_run", + custom_code = await OldCustomEvaluationDB.find_one( + OldCustomEvaluationDB.id + == PydanticObjectId( + old_eval.evaluation_type_settings.custom_code_evaluation_id + ) ) - if eval_config is not None: - list_of_eval_configs.append(eval_config.id) + eval_config = EvaluatorConfigDB( + app=app_db, + organization=app_db.organization, + user=app_db.user, + name=f"{app_db.app_name}_custom_code_default", + evaluator_key="auto_custom_code_run", + settings_values=dict({"code": custom_code.python_code}), + ) + await eval_config.insert(session=session) + list_of_eval_configs.append(eval_config.id) if evaluation_type == "auto_exact_match": eval_config = await EvaluatorConfigDB.find_one( From ff281fb994578ae51eeb31897f360ed087328746 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 17 Jan 2024 11:17:29 +0100 Subject: [PATCH 244/267] Update - added backward compatibility for old templates --- .../agenta_backend/services/llm_apps_service.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/agenta-backend/agenta_backend/services/llm_apps_service.py b/agenta-backend/agenta_backend/services/llm_apps_service.py index 67115ad5ef..59b826c528 100644 --- a/agenta-backend/agenta_backend/services/llm_apps_service.py +++ b/agenta-backend/agenta_backend/services/llm_apps_service.py @@ -86,8 +86,13 @@ async def invoke_app( ) response.raise_for_status() - lm_app_response = response.json() - return AppOutput(output=lm_app_response["message"], status="success") + llm_app_response = response.json() + app_output = ( + llm_app_response["message"] + if isinstance(llm_app_response, dict) + else llm_app_response + ) + return AppOutput(output=app_output, status="success") async def run_with_retry( From 1dc208260a893a5a59a002957d024c268afad8fd Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 17 Jan 2024 11:52:32 +0100 Subject: [PATCH 245/267] Update - modify code to create custom code evaluator config --- .../migrations/20240110165900_evaluations_revamp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index f370cf6ad8..9784a84594 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -210,10 +210,10 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): ) ) eval_config = EvaluatorConfigDB( - app=app_db, - organization=app_db.organization, - user=app_db.user, - name=f"{app_db.app_name}_custom_code_default", + app=old_eval.app, + organization=old_eval.organization, + user=old_eval.user, + name=f"{old_eval.app.app_name}_custom_code_default", evaluator_key="auto_custom_code_run", settings_values=dict({"code": custom_code.python_code}), ) From 3a223b31fd733a17eee18f55f4960388e6a2eef5 Mon Sep 17 00:00:00 2001 From: Abram Date: Wed, 17 Jan 2024 13:57:12 +0100 Subject: [PATCH 246/267] Update - fix update app variant db manager --- agenta-backend/agenta_backend/services/db_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-backend/agenta_backend/services/db_manager.py b/agenta-backend/agenta_backend/services/db_manager.py index 58f2ef984d..60bd665db8 100644 --- a/agenta-backend/agenta_backend/services/db_manager.py +++ b/agenta-backend/agenta_backend/services/db_manager.py @@ -1474,7 +1474,7 @@ async def update_app_variant( if hasattr(app_variant, key): setattr(app_variant, key, value) - await app_variant.update() + await app_variant.save() return app_variant From cd55f03b109fbd607457dc2748415e0d086e25c5 Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Wed, 17 Jan 2024 14:07:45 +0100 Subject: [PATCH 247/267] add migration for exact match evaluator --- ...547_create_exact_match_evaluator_config.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 agenta-backend/agenta_backend/migrations/20240110132547_create_exact_match_evaluator_config.py diff --git a/agenta-backend/agenta_backend/migrations/20240110132547_create_exact_match_evaluator_config.py b/agenta-backend/agenta_backend/migrations/20240110132547_create_exact_match_evaluator_config.py new file mode 100644 index 0000000000..82aed942cf --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/20240110132547_create_exact_match_evaluator_config.py @@ -0,0 +1,75 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + + +from beanie.operators import In +from pydantic import BaseModel, Field +from beanie import free_fall_migration, Document, Link, PydanticObjectId + + +class UserDB(Document): + uid: str = Field(default="0", unique=True, index=True) + username: str = Field(default="agenta") + email: str = Field(default="demo@agenta.ai", unique=True) + organizations: Optional[List[PydanticObjectId]] = [] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "users" + + +class OrganizationDB(Document): + name: str = Field(default="agenta") + description: str = Field(default="") + type: Optional[str] + owner: str # user id + members: Optional[List[PydanticObjectId]] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "organizations" + + +class AppDB(Document): + app_name: str + organization: Link[OrganizationDB] + user: Link[UserDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "app_db" + + +class EvaluatorConfigDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + name: str + evaluator_key: str + settings_values: Optional[Dict[str, Any]] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluators_configs" + + +class Forward: + @free_fall_migration( + document_models=[AppDB, UserDB, OrganizationDB, EvaluatorConfigDB] + ) + async def create_default_exact_match_evaluator(self, session): + apps_db = await AppDB.find(fetch_links=True).to_list() + for app_db in apps_db: + eval_exact_match_config = EvaluatorConfigDB( + app=app_db, + organization=app_db.organization, + user=app_db.user, + name=f"Exact Match", + evaluator_key="auto_exact_match", + settings_values={}, + ) + await eval_exact_match_config.insert(session=session) From 6e6ab609c4f03d09f79ecc9a1cf4251850a7e8ec Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Wed, 17 Jan 2024 14:08:09 +0100 Subject: [PATCH 248/267] improve evaluator name --- .../20240110165900_evaluations_revamp.py | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 9784a84594..7e198167d7 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -167,21 +167,6 @@ class Forward: ) async def migrate_old_evaluation_to_new_evaluation(self, session): # STEP 1: - # Retrieve all the apps. - # Generate an "exact_match" evaluator and a code evaluator for each app. - apps_db = await AppDB.find(fetch_links=True).to_list() - for app_db in apps_db: - eval_exact_match_config = EvaluatorConfigDB( - app=app_db, - organization=app_db.organization, - user=app_db.user, - name=f"{app_db.app_name}_exact_match_default", - evaluator_key="auto_exact_match", - settings_values={}, - ) - await eval_exact_match_config.insert(session=session) - - # STEP 2: # Review the evaluations and create a unique evaluation for each one. old_evaluations = await OldEvaluationDB.find( In( @@ -213,7 +198,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): app=old_eval.app, organization=old_eval.organization, user=old_eval.user, - name=f"{old_eval.app.app_name}_custom_code_default", + name="Custom Code Run", evaluator_key="auto_custom_code_run", settings_values=dict({"code": custom_code.python_code}), ) @@ -233,7 +218,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): app=old_eval.app, organization=old_eval.organization, user=old_eval.user, - name=f"{old_eval.app.app_name}_{evaluation_type}", + name="Similarity Match", evaluator_key=evaluation_type, settings_values=dict( { @@ -251,7 +236,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): app=old_eval.app, organization=old_eval.organization, user=old_eval.user, - name=f"{old_eval.app.app_name}_{evaluation_type}", + name="Regex Test", evaluator_key=evaluation_type, settings_values=dict( { @@ -268,7 +253,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): app=old_eval.app, organization=old_eval.organization, user=old_eval.user, - name=f"{old_eval.app.app_name}_{evaluation_type}", + name="Webhook Test", evaluator_key=evaluation_type, settings_values=dict( { @@ -285,7 +270,7 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): app=old_eval.app, organization=old_eval.organization, user=old_eval.user, - name=f"{old_eval.app.app_name}_{evaluation_type}", + name="AI Critique", evaluator_key=evaluation_type, settings_values=dict( { From 52de18e06f95efde492e73fa893cbb4f0346f2c1 Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Wed, 17 Jan 2024 14:08:27 +0100 Subject: [PATCH 249/267] remove verbose print --- .../20240113131802_new_evaluation_results_aggregation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py index 2eb249ab93..722d185e40 100644 --- a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py +++ b/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py @@ -254,7 +254,6 @@ async def aggregate_new_evaluation_with_evaluation_scenario_results(self, sessio ) print("### len evaluation_keyvalue_store", len(evaluation_keyvalue_store)) - print("EKVS: ", evaluation_keyvalue_store) await asyncio.sleep(2) # STEP 2: From d206a4c0758b5064127da6fd6879cc217b7d85a0 Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Wed, 17 Jan 2024 20:43:20 +0100 Subject: [PATCH 250/267] ignore evals with deleted apps --- .../migrations/20240110165900_evaluations_revamp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index 7e198167d7..d1af3eb729 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -183,6 +183,8 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): fetch_links=True, ).to_list() for old_eval in old_evaluations: + if getattr(old_eval, 'id', None) and not getattr(getattr(old_eval, 'app', None), 'id', None): + continue list_of_eval_configs = [] evaluation_type = old_eval.evaluation_type # Use the created evaluator if the evaluation uses "exact_match" or a code evaluator. From 887d5e99396c65443b400cebc967fc76bb841929 Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Wed, 17 Jan 2024 20:44:00 +0100 Subject: [PATCH 251/267] move human evals to separated migrations --- ...40112120721_evaluation_scenarios_revamp.py | 116 +------ ...12120740_human_a_b_evaluation_scenarios.py | 292 ++++++++++++++++++ ...human_single_model_evaluation_scenarios.py | 292 ++++++++++++++++++ 3 files changed, 586 insertions(+), 114 deletions(-) create mode 100644 agenta-backend/agenta_backend/migrations/20240112120740_human_a_b_evaluation_scenarios.py create mode 100644 agenta-backend/agenta_backend/migrations/20240112120800_human_single_model_evaluation_scenarios.py diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py index b34fd5c5c1..b771096c7d 100644 --- a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py @@ -265,7 +265,8 @@ async def migrate_old_auto_evaluation_scenario_to_new_auto_evaluation_scenario( fetch_links=True, ).to_list() for new_evaluation in new_evaluations: - for old_scenario in old_auto_scenarios: + for i, old_scenario in enumerate(old_auto_scenarios): + print(f"auto evaluation {i}") if new_evaluation.id == old_scenario.evaluation.id: results = [ EvaluationScenarioResult( @@ -313,118 +314,5 @@ async def migrate_old_auto_evaluation_scenario_to_new_auto_evaluation_scenario( ) await new_scenario.insert(session=session) - @free_fall_migration( - document_models=[ - AppDB, - OrganizationDB, - UserDB, - TestSetDB, - EvaluationDB, - OldEvaluationDB, - OldEvaluationScenarioDB, - EvaluationScenarioDB, - HumanEvaluationDB, - HumanEvaluationScenarioDB, - ] - ) - async def migrate_old_human_a_b_evaluation_scenario_to_new_human_evaluation_scenario( - self, session - ): - old_human_ab_testing_scenarios = await OldEvaluationScenarioDB.find( - OldEvaluationScenarioDB.evaluation.evaluation_type == "human_a_b_testing", - fetch_links=True, - ).to_list() - for ab_testing_scenario in old_human_ab_testing_scenarios: - matching_human_evaluation = await HumanEvaluationDB.find_one( - HumanEvaluationDB.id == ab_testing_scenario.evaluation.id, - HumanEvaluationDB.evaluation_type == "human_a_b_testing", - fetch_links=True, - ) - if matching_human_evaluation: - scenario_inputs = [ - HumanEvaluationScenarioInput( - input_name=input.input_name, - input_value=input.input_value, - ) - for input in ab_testing_scenario.inputs - ] - scenario_outputs = [ - HumanEvaluationScenarioOutput( - variant_id=output.variant_id, - variant_output=output.variant_output, - ) - for output in ab_testing_scenario.outputs - ] - new_scenario = HumanEvaluationScenarioDB( - user=matching_human_evaluation.user, - organization=matching_human_evaluation.organization, - evaluation=matching_human_evaluation, - inputs=scenario_inputs, - outputs=scenario_outputs, - correct_answer=ab_testing_scenario.correct_answer, - is_pinned=ab_testing_scenario.is_pinned, - note=ab_testing_scenario.note, - vote=ab_testing_scenario.vote, - score=ab_testing_scenario.score, - ) - await new_scenario.insert(session=session) - - @free_fall_migration( - document_models=[ - AppDB, - OrganizationDB, - UserDB, - TestSetDB, - EvaluationDB, - OldEvaluationDB, - OldEvaluationScenarioDB, - EvaluationScenarioDB, - HumanEvaluationDB, - HumanEvaluationScenarioDB, - ] - ) - async def migrate_old_human_single_model_evaluation_scenario_to_new_human_evaluation_scenario( - self, session - ): - old_human_single_model_scenarios = await OldEvaluationScenarioDB.find( - OldEvaluationScenarioDB.evaluation.evaluation_type == "single_model_test", - fetch_links=True, - ).to_list() - for single_model_scenario in old_human_single_model_scenarios: - matching_human_evaluation = await HumanEvaluationDB.find_one( - HumanEvaluationDB.id == single_model_scenario.evaluation.id, - HumanEvaluationDB.evaluation_type == "single_model_test", - fetch_links=True, - ) - if matching_human_evaluation: - scenario_inputs = [ - HumanEvaluationScenarioInput( - input_name=input.input_name, - input_value=input.input_value, - ) - for input in single_model_scenario.inputs - ] - scenario_outputs = [ - HumanEvaluationScenarioOutput( - variant_id=output.variant_id, - variant_output=output.variant_output, - ) - for output in single_model_scenario.outputs - ] - new_scenario = HumanEvaluationScenarioDB( - user=matching_human_evaluation.user, - organization=matching_human_evaluation.organization, - evaluation=matching_human_evaluation, - inputs=scenario_inputs, - outputs=scenario_outputs, - correct_answer=single_model_scenario.correct_answer, - is_pinned=single_model_scenario.is_pinned, - note=single_model_scenario.note, - vote=single_model_scenario.vote, - score=single_model_scenario.score, - ) - await new_scenario.insert(session=session) - - class Backward: pass diff --git a/agenta-backend/agenta_backend/migrations/20240112120740_human_a_b_evaluation_scenarios.py b/agenta-backend/agenta_backend/migrations/20240112120740_human_a_b_evaluation_scenarios.py new file mode 100644 index 0000000000..1802374cd2 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/20240112120740_human_a_b_evaluation_scenarios.py @@ -0,0 +1,292 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + +from beanie.operators import In +from pydantic import BaseModel, Field +from beanie import free_fall_migration, Document, Link, PydanticObjectId + + +class OrganizationDB(Document): + name: str = Field(default="agenta") + description: str = Field(default="") + type: Optional[str] + owner: str # user id + members: Optional[List[PydanticObjectId]] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "organizations" + + +class UserDB(Document): + uid: str = Field(default="0", unique=True, index=True) + username: str = Field(default="agenta") + email: str = Field(default="demo@agenta.ai", unique=True) + organizations: Optional[List[PydanticObjectId]] = [] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "users" + + +class AppDB(Document): + app_name: str + organization: Link[OrganizationDB] + user: Link[UserDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "app_db" + + +class TestSetDB(Document): + name: str + app: Link[AppDB] + csvdata: List[Dict[str, str]] + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "testsets" + + +class EvaluatorConfigDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + name: str + evaluator_key: str + settings_values: Optional[Dict[str, Any]] = None + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluators_configs" + + +class Result(BaseModel): + type: str + value: Any + + +class EvaluationScenarioResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class AggregatedResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class EvaluationScenarioInputDB(BaseModel): + name: str + type: str + value: str + + +class EvaluationScenarioOutputDB(BaseModel): + type: str + value: Any + + +class HumanEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class HumanEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class HumanEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + variants: List[PydanticObjectId] + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "human_evaluations" + + +class HumanEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[HumanEvaluationDB] + inputs: List[HumanEvaluationScenarioInput] + outputs: List[HumanEvaluationScenarioOutput] + vote: Optional[str] + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "human_evaluations_scenarios" + + +class EvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str = Field(default="EVALUATION_INITIALIZED") + testset: Link[TestSetDB] + variant: PydanticObjectId + evaluators_configs: List[PydanticObjectId] + aggregated_results: List[AggregatedResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "new_evaluations" + + +class EvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[EvaluationDB] + variant_id: PydanticObjectId + inputs: List[EvaluationScenarioInputDB] + outputs: List[EvaluationScenarioOutputDB] + correct_answer: Optional[str] + is_pinned: Optional[bool] + note: Optional[str] + evaluators_configs: List[PydanticObjectId] + results: List[EvaluationScenarioResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "new_evaluation_scenarios" + + +class OldEvaluationTypeSettings(BaseModel): + similarity_threshold: Optional[float] + regex_pattern: Optional[str] + regex_should_match: Optional[bool] + webhook_url: Optional[str] + llm_app_prompt_template: Optional[str] + custom_code_evaluation_id: Optional[str] + evaluation_prompt_template: Optional[str] + + +class OldEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class OldEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class OldEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + evaluation_type_settings: OldEvaluationTypeSettings + variants: List[PydanticObjectId] + version: str = Field("odmantic") + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluations" + + +class OldEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[OldEvaluationDB] + inputs: List[OldEvaluationScenarioInput] + outputs: List[OldEvaluationScenarioOutput] # EvaluationScenarioOutput + vote: Optional[str] + version: str = Field("odmantic") + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "evaluation_scenarios" + + +class Forward: + @free_fall_migration( + document_models=[ + AppDB, + OrganizationDB, + UserDB, + TestSetDB, + EvaluationDB, + OldEvaluationDB, + OldEvaluationScenarioDB, + EvaluationScenarioDB, + HumanEvaluationDB, + HumanEvaluationScenarioDB, + ] + ) + async def migrate_old_human_a_b_evaluation_scenario_to_new_human_evaluation_scenario( + self, session + ): + old_human_ab_testing_scenarios = await OldEvaluationScenarioDB.find( + OldEvaluationScenarioDB.evaluation.evaluation_type == "human_a_b_testing", + fetch_links=True, + ).to_list() + for counter, ab_testing_scenario in enumerate(old_human_ab_testing_scenarios): + print(f"ab evaluation scenario {counter}") + matching_human_evaluation = await HumanEvaluationDB.find_one( + HumanEvaluationDB.id == ab_testing_scenario.evaluation.id, + HumanEvaluationDB.evaluation_type == "human_a_b_testing", + fetch_links=True, + ) + if matching_human_evaluation: + scenario_inputs = [ + HumanEvaluationScenarioInput( + input_name=input.input_name, + input_value=input.input_value, + ) + for input in ab_testing_scenario.inputs + ] + scenario_outputs = [ + HumanEvaluationScenarioOutput( + variant_id=output.variant_id, + variant_output=output.variant_output, + ) + for output in ab_testing_scenario.outputs + ] + new_scenario = HumanEvaluationScenarioDB( + user=matching_human_evaluation.user, + organization=matching_human_evaluation.organization, + evaluation=matching_human_evaluation, + inputs=scenario_inputs, + outputs=scenario_outputs, + correct_answer=ab_testing_scenario.correct_answer, + is_pinned=ab_testing_scenario.is_pinned, + note=ab_testing_scenario.note, + vote=ab_testing_scenario.vote, + score=ab_testing_scenario.score, + ) + await new_scenario.insert(session=session) + + +class Backward: + pass diff --git a/agenta-backend/agenta_backend/migrations/20240112120800_human_single_model_evaluation_scenarios.py b/agenta-backend/agenta_backend/migrations/20240112120800_human_single_model_evaluation_scenarios.py new file mode 100644 index 0000000000..6fa1178183 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/20240112120800_human_single_model_evaluation_scenarios.py @@ -0,0 +1,292 @@ +from datetime import datetime +from typing import Any, Dict, List, Optional + +from beanie.operators import In +from pydantic import BaseModel, Field +from beanie import free_fall_migration, Document, Link, PydanticObjectId + + +class OrganizationDB(Document): + name: str = Field(default="agenta") + description: str = Field(default="") + type: Optional[str] + owner: str # user id + members: Optional[List[PydanticObjectId]] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "organizations" + + +class UserDB(Document): + uid: str = Field(default="0", unique=True, index=True) + username: str = Field(default="agenta") + email: str = Field(default="demo@agenta.ai", unique=True) + organizations: Optional[List[PydanticObjectId]] = [] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "users" + + +class AppDB(Document): + app_name: str + organization: Link[OrganizationDB] + user: Link[UserDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "app_db" + + +class TestSetDB(Document): + name: str + app: Link[AppDB] + csvdata: List[Dict[str, str]] + user: Link[UserDB] + organization: Link[OrganizationDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "testsets" + + +class EvaluatorConfigDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + name: str + evaluator_key: str + settings_values: Optional[Dict[str, Any]] = None + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluators_configs" + + +class Result(BaseModel): + type: str + value: Any + + +class EvaluationScenarioResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class AggregatedResult(BaseModel): + evaluator_config: PydanticObjectId + result: Result + + +class EvaluationScenarioInputDB(BaseModel): + name: str + type: str + value: str + + +class EvaluationScenarioOutputDB(BaseModel): + type: str + value: Any + + +class HumanEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class HumanEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class HumanEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + variants: List[PydanticObjectId] + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "human_evaluations" + + +class HumanEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[HumanEvaluationDB] + inputs: List[HumanEvaluationScenarioInput] + outputs: List[HumanEvaluationScenarioOutput] + vote: Optional[str] + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "human_evaluations_scenarios" + + +class EvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str = Field(default="EVALUATION_INITIALIZED") + testset: Link[TestSetDB] + variant: PydanticObjectId + evaluators_configs: List[PydanticObjectId] + aggregated_results: List[AggregatedResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "new_evaluations" + + +class EvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[EvaluationDB] + variant_id: PydanticObjectId + inputs: List[EvaluationScenarioInputDB] + outputs: List[EvaluationScenarioOutputDB] + correct_answer: Optional[str] + is_pinned: Optional[bool] + note: Optional[str] + evaluators_configs: List[PydanticObjectId] + results: List[EvaluationScenarioResult] + created_at: datetime = Field(default=datetime.utcnow()) + updated_at: datetime = Field(default=datetime.utcnow()) + + class Settings: + name = "new_evaluation_scenarios" + + +class OldEvaluationTypeSettings(BaseModel): + similarity_threshold: Optional[float] + regex_pattern: Optional[str] + regex_should_match: Optional[bool] + webhook_url: Optional[str] + llm_app_prompt_template: Optional[str] + custom_code_evaluation_id: Optional[str] + evaluation_prompt_template: Optional[str] + + +class OldEvaluationScenarioInput(BaseModel): + input_name: str + input_value: str + + +class OldEvaluationScenarioOutput(BaseModel): + variant_id: str + variant_output: str + + +class OldEvaluationDB(Document): + app: Link[AppDB] + organization: Link[OrganizationDB] + user: Link[UserDB] + status: str + evaluation_type: str + evaluation_type_settings: OldEvaluationTypeSettings + variants: List[PydanticObjectId] + version: str = Field("odmantic") + testset: Link[TestSetDB] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + + class Settings: + name = "evaluations" + + +class OldEvaluationScenarioDB(Document): + user: Link[UserDB] + organization: Link[OrganizationDB] + evaluation: Link[OldEvaluationDB] + inputs: List[OldEvaluationScenarioInput] + outputs: List[OldEvaluationScenarioOutput] # EvaluationScenarioOutput + vote: Optional[str] + version: str = Field("odmantic") + score: Optional[Any] + correct_answer: Optional[str] + created_at: Optional[datetime] = Field(default=datetime.utcnow()) + updated_at: Optional[datetime] = Field(default=datetime.utcnow()) + is_pinned: Optional[bool] + note: Optional[str] + + class Settings: + name = "evaluation_scenarios" + + +class Forward: + @free_fall_migration( + document_models=[ + AppDB, + OrganizationDB, + UserDB, + TestSetDB, + EvaluationDB, + OldEvaluationDB, + OldEvaluationScenarioDB, + EvaluationScenarioDB, + HumanEvaluationDB, + HumanEvaluationScenarioDB, + ] + ) + async def migrate_old_human_single_model_evaluation_scenario_to_new_human_evaluation_scenario( + self, session + ): + old_human_single_model_scenarios = await OldEvaluationScenarioDB.find( + OldEvaluationScenarioDB.evaluation.evaluation_type == "single_model_test", + fetch_links=True, + ).to_list() + for counter, single_model_scenario in enumerate(old_human_single_model_scenarios): + print(f"single model evaluation {counter}") + matching_human_evaluation = await HumanEvaluationDB.find_one( + HumanEvaluationDB.id == single_model_scenario.evaluation.id, + HumanEvaluationDB.evaluation_type == "single_model_test", + fetch_links=True, + ) + if matching_human_evaluation: + scenario_inputs = [ + HumanEvaluationScenarioInput( + input_name=input.input_name, + input_value=input.input_value, + ) + for input in single_model_scenario.inputs + ] + scenario_outputs = [ + HumanEvaluationScenarioOutput( + variant_id=output.variant_id, + variant_output=output.variant_output, + ) + for output in single_model_scenario.outputs + ] + new_scenario = HumanEvaluationScenarioDB( + user=matching_human_evaluation.user, + organization=matching_human_evaluation.organization, + evaluation=matching_human_evaluation, + inputs=scenario_inputs, + outputs=scenario_outputs, + correct_answer=single_model_scenario.correct_answer, + is_pinned=single_model_scenario.is_pinned, + note=single_model_scenario.note, + vote=single_model_scenario.vote, + score=single_model_scenario.score, + ) + await new_scenario.insert(session=session) + + +class Backward: + pass From 12d06e897921c2ddf0ab1479c7c250a8292f5e46 Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Wed, 17 Jan 2024 20:52:29 +0100 Subject: [PATCH 252/267] format --- agenta-web/src/lib/helpers/evaluate.ts | 2 +- .../src/pages/apps/[app_id]/testsets/new/upload/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/agenta-web/src/lib/helpers/evaluate.ts b/agenta-web/src/lib/helpers/evaluate.ts index d4962ff42c..abfac16f40 100644 --- a/agenta-web/src/lib/helpers/evaluate.ts +++ b/agenta-web/src/lib/helpers/evaluate.ts @@ -219,4 +219,4 @@ export const calculateResultsDataAvg = ( export const getVotesPercentage = (record: HumanEvaluationListTableDataType, index: number) => { const variant = record.votesData.variants[index] return record.votesData.variants_votes_data[variant]?.percentage -} \ No newline at end of file +} diff --git a/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx b/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx index 139c45ffdc..29f7c47337 100644 --- a/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx +++ b/agenta-web/src/pages/apps/[app_id]/testsets/new/upload/index.tsx @@ -70,8 +70,8 @@ export default function AddANewTestset() { router.push(`/apps/${appId}/testsets`) } catch (e: any) { if ( - e?.response?.data?.detail?.find((item: GenericObject) => - item?.loc?.includes("csvdata"), + e?.response?.data?.detail?.find( + (item: GenericObject) => item?.loc?.includes("csvdata"), ) ) message.error(malformedFileError) From 027d477aeeb2f9b125a5c78448f7db7c8f7c2060 Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Wed, 17 Jan 2024 20:53:43 +0100 Subject: [PATCH 253/267] format backend --- .../migrations/20240110165900_evaluations_revamp.py | 4 +++- .../migrations/20240112120721_evaluation_scenarios_revamp.py | 1 + .../20240112120800_human_single_model_evaluation_scenarios.py | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py index d1af3eb729..6514a18677 100644 --- a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py @@ -183,7 +183,9 @@ async def migrate_old_evaluation_to_new_evaluation(self, session): fetch_links=True, ).to_list() for old_eval in old_evaluations: - if getattr(old_eval, 'id', None) and not getattr(getattr(old_eval, 'app', None), 'id', None): + if getattr(old_eval, "id", None) and not getattr( + getattr(old_eval, "app", None), "id", None + ): continue list_of_eval_configs = [] evaluation_type = old_eval.evaluation_type diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py index b771096c7d..81911214a9 100644 --- a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py +++ b/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py @@ -314,5 +314,6 @@ async def migrate_old_auto_evaluation_scenario_to_new_auto_evaluation_scenario( ) await new_scenario.insert(session=session) + class Backward: pass diff --git a/agenta-backend/agenta_backend/migrations/20240112120800_human_single_model_evaluation_scenarios.py b/agenta-backend/agenta_backend/migrations/20240112120800_human_single_model_evaluation_scenarios.py index 6fa1178183..82c58fcb10 100644 --- a/agenta-backend/agenta_backend/migrations/20240112120800_human_single_model_evaluation_scenarios.py +++ b/agenta-backend/agenta_backend/migrations/20240112120800_human_single_model_evaluation_scenarios.py @@ -251,7 +251,9 @@ async def migrate_old_human_single_model_evaluation_scenario_to_new_human_evalua OldEvaluationScenarioDB.evaluation.evaluation_type == "single_model_test", fetch_links=True, ).to_list() - for counter, single_model_scenario in enumerate(old_human_single_model_scenarios): + for counter, single_model_scenario in enumerate( + old_human_single_model_scenarios + ): print(f"single model evaluation {counter}") matching_human_evaluation = await HumanEvaluationDB.find_one( HumanEvaluationDB.id == single_model_scenario.evaluation.id, From fb477b0f6f0de425e8dcd2c39124141d807c1fb2 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Thu, 18 Jan 2024 08:52:08 +0500 Subject: [PATCH 254/267] evaluation improvements and bug fixes --- agenta-web/dev.Dockerfile | 60 ++++----- .../cellRenderers/cellRenderers.tsx | 43 +++++- .../evaluationCompare/EvaluationCompare.tsx | 4 +- .../evaluationResults/EmptyEvaluations.tsx | 89 +++++++++++++ .../evaluationResults/EvaluationResults.tsx | 122 ++++-------------- .../EvaluationScenarios.tsx | 18 ++- .../evaluations/evaluators/EvaluatorCard.tsx | 3 + .../evaluators/NewEvaluatorModal.tsx | 28 +++- agenta-web/src/lib/Types.ts | 10 +- agenta-web/src/lib/helpers/evaluate.ts | 55 +++++++- agenta-web/src/lib/helpers/utils.ts | 11 +- agenta-web/src/media/eval-illustration.png | Bin 0 -> 10636 bytes agenta-web/src/services/evaluations/index.ts | 11 +- 13 files changed, 301 insertions(+), 153 deletions(-) create mode 100644 agenta-web/src/components/pages/evaluations/evaluationResults/EmptyEvaluations.tsx create mode 100644 agenta-web/src/media/eval-illustration.png diff --git a/agenta-web/dev.Dockerfile b/agenta-web/dev.Dockerfile index 6af573852c..ad5bc9a53b 100644 --- a/agenta-web/dev.Dockerfile +++ b/agenta-web/dev.Dockerfile @@ -1,37 +1,37 @@ FROM node:18-alpine -WORKDIR /app +# WORKDIR /app -# Install dependencies based on the preferred package manager -COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ -RUN \ - if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ - elif [ -f package-lock.json ]; then npm i; \ - elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ - # Allow install without lockfile, so example works even without Node.js installed locally - else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ - fi +# # Install dependencies based on the preferred package manager +# COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +# RUN \ +# if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ +# elif [ -f package-lock.json ]; then npm i; \ +# elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ +# # Allow install without lockfile, so example works even without Node.js installed locally +# else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ +# fi -COPY src ./src -COPY public ./public -COPY next.config.js . -COPY tsconfig.json . -COPY postcss.config.js . -COPY .env . -RUN if [ -f .env.local ]; then cp .env.local .; fi -# # used in cloud -COPY sentry.* . -# Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry -# Uncomment the following line to disable telemetry at run time -# ENV NEXT_TELEMETRY_DISABLED 1 +# COPY src ./src +# COPY public ./public +# COPY next.config.js . +# COPY tsconfig.json . +# COPY postcss.config.js . +# COPY .env . +# RUN if [ -f .env.local ]; then cp .env.local .; fi +# # # used in cloud +# COPY sentry.* . +# # Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry +# # Uncomment the following line to disable telemetry at run time +# # ENV NEXT_TELEMETRY_DISABLED 1 -# Note: Don't expose ports here, Compose will handle that for us +# # Note: Don't expose ports here, Compose will handle that for us -# Start Next.js in development mode based on the preferred package manager -CMD \ - if [ -f yarn.lock ]; then yarn dev; \ - elif [ -f package-lock.json ]; then npm run dev; \ - elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ - else yarn dev; \ - fi +# # Start Next.js in development mode based on the preferred package manager +# CMD \ +# if [ -f yarn.lock ]; then yarn dev; \ +# elif [ -f package-lock.json ]; then npm run dev; \ +# elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ +# else yarn dev; \ +# fi diff --git a/agenta-web/src/components/pages/evaluations/cellRenderers/cellRenderers.tsx b/agenta-web/src/components/pages/evaluations/cellRenderers/cellRenderers.tsx index f2263c3cbb..205253543c 100644 --- a/agenta-web/src/components/pages/evaluations/cellRenderers/cellRenderers.tsx +++ b/agenta-web/src/components/pages/evaluations/cellRenderers/cellRenderers.tsx @@ -3,9 +3,14 @@ import {EvaluationStatus, JSSTheme, _Evaluation} from "@/lib/Types" import {CopyOutlined, FullscreenExitOutlined, FullscreenOutlined} from "@ant-design/icons" import {ICellRendererParams} from "ag-grid-community" import {GlobalToken, Space, Typography, message, theme} from "antd" +import dayjs from "dayjs" +import relativeTime from "dayjs/plugin/relativeTime" +import duration from "dayjs/plugin/duration" import Link from "next/link" import React, {useCallback, useEffect, useState} from "react" import {createUseStyles} from "react-jss" +dayjs.extend(relativeTime) +dayjs.extend(duration) const useStyles = createUseStyles((theme: JSSTheme) => ({ statusCell: { @@ -71,7 +76,27 @@ export function LongTextCellRenderer(params: ICellRendererParams) { }, []) const onExpand = useCallback(() => { - node.setRowHeight(api.getSizesForCurrentTheme().rowHeight * (expanded ? 1 : 5)) + const cells = document.querySelectorAll(`[row-id='${node.id}'] .ag-cell > *`) + const cellsArr = Array.from(cells || []) + const defaultHeight = api.getSizesForCurrentTheme().rowHeight + if (!expanded) { + cellsArr.forEach((cell) => { + cell.setAttribute( + "style", + "overflow: visible; white-space: pre-wrap; text-overflow: unset;", + ) + }) + const height = Math.max(...cellsArr.map((cell) => cell.scrollHeight)) + node.setRowHeight(height <= defaultHeight ? defaultHeight * 2 : height) + } else { + cellsArr.forEach((cell) => { + cell.setAttribute( + "style", + "overflow: hidden; white-space: nowrap; text-overflow: ellipsis;", + ) + }) + node.setRowHeight(defaultHeight) + } api.onRowHeightChanged() }, [expanded]) @@ -147,3 +172,19 @@ export const LinkCellRenderer = React.memo( }, (prev, next) => prev.value === next.value && prev.href === next.href, ) + +export const DateFromNowRenderer = React.memo( + (params: ICellRendererParams) => { + const [date, setDate] = useState(params.value) + + useEffect(() => { + const interval = setInterval(() => { + setDate((date: any) => dayjs(date).add(1, "second").valueOf()) + }, 60000) + return () => clearInterval(interval) + }, []) + + return {dayjs(date).fromNow()} + }, + (prev, next) => prev.value === next.value, +) diff --git a/agenta-web/src/components/pages/evaluations/evaluationCompare/EvaluationCompare.tsx b/agenta-web/src/components/pages/evaluations/evaluationCompare/EvaluationCompare.tsx index 4c52a2302d..4bd578fb45 100644 --- a/agenta-web/src/components/pages/evaluations/evaluationCompare/EvaluationCompare.tsx +++ b/agenta-web/src/components/pages/evaluations/evaluationCompare/EvaluationCompare.tsx @@ -14,7 +14,7 @@ import {AgGridReact} from "ag-grid-react" import {Space, Spin, Tag, Tooltip, Typography} from "antd" import React, {useEffect, useMemo, useRef, useState} from "react" import {createUseStyles} from "react-jss" -import {getFilterParams, getTypedValue} from "../evaluationResults/EvaluationResults" +import {getFilterParams, getTypedValue} from "@/lib/helpers/evaluate" import {getColorFromStr, getRandomColors} from "@/lib/helpers/colors" import {DownloadOutlined} from "@ant-design/icons" import {getAppValues} from "@/contexts/app.context" @@ -209,8 +209,6 @@ const EvaluationCompareMode: React.FC = () => { fetcher() }, [appId, evaluationIdsStr]) - // useAgGridCustomHeaders(gridRef.current?.api) - const handleDeleteVariant = (evalId: string) => { setEvaluationIdsStr(evaluationIds.filter((item) => item !== evalId).join(",")) } diff --git a/agenta-web/src/components/pages/evaluations/evaluationResults/EmptyEvaluations.tsx b/agenta-web/src/components/pages/evaluations/evaluationResults/EmptyEvaluations.tsx new file mode 100644 index 0000000000..cc4e45d87b --- /dev/null +++ b/agenta-web/src/components/pages/evaluations/evaluationResults/EmptyEvaluations.tsx @@ -0,0 +1,89 @@ +import {JSSTheme} from "@/lib/Types" +import {PlusCircleOutlined, SlidersOutlined} from "@ant-design/icons" +import {Button, Empty, Space, Tooltip, Typography} from "antd" +import Image from "next/image" +import React from "react" +import {createUseStyles} from "react-jss" +import evaluationIllustration from "@/media/eval-illustration.png" + +const useStyles = createUseStyles((theme: JSSTheme) => ({ + emptyRoot: { + height: "calc(100vh - 260px)", + display: "grid", + placeItems: "center", + }, + empty: { + "& .ant-empty-description": { + fontSize: 18, + marginTop: "0.75rem", + marginBottom: "1.5rem", + }, + }, + emptyImg: { + width: 120, + height: 120, + objectFit: "contain", + filter: theme.isDark ? "invert(1)" : "none", + opacity: 0.85, + }, +})) + +interface Props { + onConfigureEvaluators?: () => void + onBeginEvaluation?: () => void +} + +const EmptyEvaluations: React.FC = ({onConfigureEvaluators, onBeginEvaluation}) => { + const classes = useStyles() + + return ( +
+ + Welcome to the evaluation setup! +
+ Are you ready to get started? + + } + image={ + no evaluation illustration + } + > + + + + + Or + + + + +
+
+ ) +} + +export default EmptyEvaluations diff --git a/agenta-web/src/components/pages/evaluations/evaluationResults/EvaluationResults.tsx b/agenta-web/src/components/pages/evaluations/evaluationResults/EvaluationResults.tsx index a1f9ea2590..292186c7c4 100644 --- a/agenta-web/src/components/pages/evaluations/evaluationResults/EvaluationResults.tsx +++ b/agenta-web/src/components/pages/evaluations/evaluationResults/EvaluationResults.tsx @@ -3,10 +3,10 @@ import {AgGridReact} from "ag-grid-react" import {useAppTheme} from "@/components/Layout/ThemeContextProvider" import {ColDef} from "ag-grid-community" import {createUseStyles} from "react-jss" -import {Button, Empty, Space, Spin, Tag, Tooltip, Typography, theme} from "antd" +import {Button, Space, Spin, Tag, Tooltip, theme} from "antd" import {DeleteOutlined, PlusCircleOutlined, SlidersOutlined, SwapOutlined} from "@ant-design/icons" -import {EvaluationStatus, GenericObject, JSSTheme, TypedValue, _Evaluation} from "@/lib/Types" -import {capitalize, round, uniqBy} from "lodash" +import {EvaluationStatus, JSSTheme, _Evaluation} from "@/lib/Types" +import {uniqBy} from "lodash" import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime" import duration from "dayjs/plugin/duration" @@ -17,6 +17,7 @@ import {useUpdateEffect} from "usehooks-ts" import {shortPoll} from "@/lib/helpers/utils" import AlertPopup from "@/components/AlertPopup/AlertPopup" import { + DateFromNowRenderer, LinkCellRenderer, StatusRenderer, runningStatuses, @@ -26,15 +27,12 @@ import {useAtom} from "jotai" import {evaluatorsAtom} from "@/lib/atoms/evaluation" import AgCustomHeader from "@/components/AgCustomHeader/AgCustomHeader" import {useRouter} from "next/router" +import EmptyEvaluations from "./EmptyEvaluations" +import {calcEvalDuration, getFilterParams, getTypedValue} from "@/lib/helpers/evaluate" dayjs.extend(relativeTime) dayjs.extend(duration) const useStyles = createUseStyles((theme: JSSTheme) => ({ - emptyRoot: { - height: "calc(100vh - 260px)", - display: "grid", - placeItems: "center", - }, root: { display: "flex", flexDirection: "column", @@ -49,56 +47,6 @@ const useStyles = createUseStyles((theme: JSSTheme) => ({ }, })) -export function getTypedValue(res?: TypedValue) { - const {value, type} = res || {} - return type === "number" - ? round(Number(value), 2) - : ["boolean", "bool"].includes(type as string) - ? capitalize(value?.toString()) - : value?.toString() -} - -export function getFilterParams(type: "number" | "text" | "date") { - const filterParams: GenericObject = {} - if (type == "date") { - filterParams.comparator = function ( - filterLocalDateAtMidnight: Date, - cellValue: string | null, - ) { - if (cellValue == null) return -1 - const cellDate = dayjs(cellValue).startOf("day").toDate() - if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) { - return 0 - } - if (cellDate < filterLocalDateAtMidnight) { - return -1 - } - if (cellDate > filterLocalDateAtMidnight) { - return 1 - } - } - } - - return { - sortable: true, - floatingFilter: true, - filter: - type === "number" - ? "agNumberColumnFilter" - : type === "date" - ? "agDateColumnFilter" - : "agTextColumnFilter", - cellDataType: type, - filterParams, - } -} - -export const calcEvalDuration = (evaluation: _Evaluation) => { - return dayjs( - runningStatuses.includes(evaluation.status) ? Date.now() : evaluation.updated_at, - ).diff(dayjs(evaluation.created_at), "milliseconds") -} - interface Props {} const EvaluationResults: React.FC = () => { @@ -197,6 +145,17 @@ const EvaluationResults: React.FC = () => { [evaluations], ) + const compareDisabled = useMemo( + () => + selected.length < 2 || + selected.some( + (item) => + item.status !== EvaluationStatus.FINISHED || + item.testset.id !== selected[0].testset.id, + ), + [selected], + ) + const colDefs = useMemo(() => { const colDefs: ColDef<_Evaluation>[] = [ { @@ -283,23 +242,13 @@ const EvaluationResults: React.FC = () => { headerName: "Created", minWidth: 160, ...getFilterParams("date"), - valueFormatter: (params) => dayjs(params.value).fromNow(), + cellRenderer: DateFromNowRenderer, + sort: "desc", }, ] return colDefs }, [evaluatorConfigs]) - const compareDisabled = useMemo( - () => - selected.length < 2 || - selected.some( - (item) => - item.status !== EvaluationStatus.FINISHED || - item.testset.id !== selected[0].testset.id, - ), - [selected], - ) - const compareBtnNode = ( - Or - - - -
+ + router.push(`/apps/${appId}/evaluations?tab=evaluators`) + } + onBeginEvaluation={() => { + setNewEvalModalOpen(true) + }} + /> ) : (
diff --git a/agenta-web/src/components/pages/evaluations/evaluationScenarios/EvaluationScenarios.tsx b/agenta-web/src/components/pages/evaluations/evaluationScenarios/EvaluationScenarios.tsx index dc18742a60..44e827458b 100644 --- a/agenta-web/src/components/pages/evaluations/evaluationScenarios/EvaluationScenarios.tsx +++ b/agenta-web/src/components/pages/evaluations/evaluationScenarios/EvaluationScenarios.tsx @@ -1,7 +1,11 @@ import {useAppTheme} from "@/components/Layout/ThemeContextProvider" import {useAppId} from "@/hooks/useAppId" import {JSSTheme, _Evaluation, _EvaluationScenario} from "@/lib/Types" -import {deleteEvaluations, fetchAllEvaluationScenarios} from "@/services/evaluations" +import { + deleteEvaluations, + fetchAllEvaluationScenarios, + fetchAllEvaluators, +} from "@/services/evaluations" import {DeleteOutlined, DownloadOutlined} from "@ant-design/icons" import {ColDef} from "ag-grid-community" import {AgGridReact} from "ag-grid-react" @@ -9,7 +13,7 @@ import {Space, Spin, Tag, Tooltip, Typography} from "antd" import {useRouter} from "next/router" import React, {useEffect, useMemo, useRef, useState} from "react" import {createUseStyles} from "react-jss" -import {getFilterParams, getTypedValue} from "../evaluationResults/EvaluationResults" +import {getFilterParams, getTypedValue} from "@/lib/helpers/evaluate" import {getAppValues} from "@/contexts/app.context" import AlertPopup from "@/components/AlertPopup/AlertPopup" import {formatDate} from "@/lib/helpers/dateTimeHelper" @@ -46,7 +50,7 @@ const EvaluationScenarios: React.FC = () => { const evaluationId = router.query.evaluation_id as string const [scenarios, setScenarios] = useState<_EvaluationScenario[]>([]) const [fetching, setFetching] = useState(false) - const [evaluators] = useAtom(evaluatorsAtom) + const [evaluators, setEvaluators] = useAtom(evaluatorsAtom) const gridRef = useRef>() const evalaution = scenarios[0]?.evaluation @@ -121,9 +125,13 @@ const EvaluationScenarios: React.FC = () => { const fetcher = () => { setFetching(true) - fetchAllEvaluationScenarios(evaluationId) - .then((scenarios) => { + Promise.all([ + evaluators.length ? Promise.resolve(evaluators) : fetchAllEvaluators(), + fetchAllEvaluationScenarios(evaluationId), + ]) + .then(([evaluators, scenarios]) => { setScenarios(scenarios) + setEvaluators(evaluators) setTimeout(() => { if (!gridRef.current) return diff --git a/agenta-web/src/components/pages/evaluations/evaluators/EvaluatorCard.tsx b/agenta-web/src/components/pages/evaluations/evaluators/EvaluatorCard.tsx index d3aba48c20..5e840cafeb 100644 --- a/agenta-web/src/components/pages/evaluations/evaluators/EvaluatorCard.tsx +++ b/agenta-web/src/components/pages/evaluations/evaluators/EvaluatorCard.tsx @@ -12,8 +12,11 @@ import {evaluatorsAtom} from "@/lib/atoms/evaluation" const useStyles = createUseStyles((theme: JSSTheme) => ({ card: { + display: "flex", + flexDirection: "column", "& .ant-card-body": { padding: "1.25rem 0.75rem 1rem 1rem", + flex: 1, }, }, body: { diff --git a/agenta-web/src/components/pages/evaluations/evaluators/NewEvaluatorModal.tsx b/agenta-web/src/components/pages/evaluations/evaluators/NewEvaluatorModal.tsx index 27ab99a054..03dd8d3afc 100644 --- a/agenta-web/src/components/pages/evaluations/evaluators/NewEvaluatorModal.tsx +++ b/agenta-web/src/components/pages/evaluations/evaluators/NewEvaluatorModal.tsx @@ -51,6 +51,11 @@ const useStyles = createUseStyles((theme: JSSTheme) => ({ margin: "1rem -1.5rem", width: "unset", }, + editor: { + border: `1px solid ${theme.colorBorder}`, + borderRadius: theme.borderRadius, + overflow: "hidden", + }, })) type DynamicFormFieldProps = EvaluationSettingsTemplate & { @@ -95,13 +100,28 @@ const DynamicFormField: React.FC = ({ {type === "string" || type === "regex" ? ( ) : type === "number" ? ( - - ) : type === "boolean" ? ( + + ) : type === "boolean" || type === "bool" ? ( ) : type === "text" ? ( ) : type === "code" ? ( - + + ) : type === "object" ? ( + ) : null} ) @@ -166,7 +186,7 @@ const NewEvaluatorModal: React.FC = ({ icon: editMode ? : , loading: submitLoading, }} - width={540} + width={650} {...props} > diff --git a/agenta-web/src/lib/Types.ts b/agenta-web/src/lib/Types.ts index e3634fbfbd..7367677f9b 100644 --- a/agenta-web/src/lib/Types.ts +++ b/agenta-web/src/lib/Types.ts @@ -301,7 +301,15 @@ export type ChatMessage = { } type ValueType = number | string | boolean | GenericObject | null -type ValueTypeOptions = "text" | "number" | "boolean" | "bool" | "string" | "code" | "regex" +type ValueTypeOptions = + | "text" + | "number" + | "boolean" + | "bool" + | "string" + | "code" + | "regex" + | "object" //evaluation revamp types export interface EvaluationSettingsTemplate { diff --git a/agenta-web/src/lib/helpers/evaluate.ts b/agenta-web/src/lib/helpers/evaluate.ts index fb1b932da6..13e3241186 100644 --- a/agenta-web/src/lib/helpers/evaluate.ts +++ b/agenta-web/src/lib/helpers/evaluate.ts @@ -1,6 +1,9 @@ import {HumanEvaluationListTableDataType} from "@/components/Evaluations/HumanEvaluationResult" -import {Evaluation, GenericObject, Variant} from "../Types" +import {Evaluation, GenericObject, TypedValue, Variant, _Evaluation} from "../Types" import {convertToCsv, downloadCsv} from "./fileManipulations" +import {capitalize, round} from "lodash" +import dayjs from "dayjs" +import {runningStatuses} from "@/components/pages/evaluations/cellRenderers/cellRenderers" export const exportABTestingEvaluationData = (evaluation: Evaluation, rows: GenericObject[]) => { const exportRow = rows.map((data, ix) => { @@ -64,3 +67,53 @@ export const getVotesPercentage = (record: HumanEvaluationListTableDataType, ind const variant = record.votesData.variants[index] return record.votesData.variants_votes_data[variant]?.percentage } + +export function getTypedValue(res?: TypedValue) { + const {value, type} = res || {} + return type === "number" + ? round(Number(value), 2) + : ["boolean", "bool"].includes(type as string) + ? capitalize(value?.toString()) + : value?.toString() +} + +export function getFilterParams(type: "number" | "text" | "date") { + const filterParams: GenericObject = {} + if (type == "date") { + filterParams.comparator = function ( + filterLocalDateAtMidnight: Date, + cellValue: string | null, + ) { + if (cellValue == null) return -1 + const cellDate = dayjs(cellValue).startOf("day").toDate() + if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) { + return 0 + } + if (cellDate < filterLocalDateAtMidnight) { + return -1 + } + if (cellDate > filterLocalDateAtMidnight) { + return 1 + } + } + } + + return { + sortable: true, + floatingFilter: true, + filter: + type === "number" + ? "agNumberColumnFilter" + : type === "date" + ? "agDateColumnFilter" + : "agTextColumnFilter", + cellDataType: type, + filterParams, + } +} + +export const calcEvalDuration = (evaluation: _Evaluation) => { + return dayjs( + runningStatuses.includes(evaluation.status) ? Date.now() : evaluation.updated_at, + ).diff(dayjs(evaluation.created_at), "milliseconds") +} diff --git a/agenta-web/src/lib/helpers/utils.ts b/agenta-web/src/lib/helpers/utils.ts index eff5b6e6ca..5c53716bb6 100644 --- a/agenta-web/src/lib/helpers/utils.ts +++ b/agenta-web/src/lib/helpers/utils.ts @@ -355,11 +355,12 @@ export function pickRandom(arr: T[], len: number) { return result } -export function durationToStr(duration: number) { - const days = Math.floor(dayjs.duration(duration, "milliseconds").asDays()) - const hours = Math.floor(dayjs.duration(duration, "milliseconds").asHours()) - const mins = Math.floor(dayjs.duration(duration, "milliseconds").asMinutes()) - const secs = Math.floor(dayjs.duration(duration, "milliseconds").asSeconds()) +export function durationToStr(ms: number) { + const duration = dayjs.duration(ms, "milliseconds") + const days = Math.floor(duration.asDays()) + const hours = Math.floor(duration.asHours() % 24) + const mins = Math.floor(duration.asMinutes() % 60) + const secs = Math.floor(duration.asSeconds() % 60) if (days > 0) return `${days}d ${hours}h` if (hours > 0) return `${hours}h ${mins}m` diff --git a/agenta-web/src/media/eval-illustration.png b/agenta-web/src/media/eval-illustration.png new file mode 100644 index 0000000000000000000000000000000000000000..6565954d4a65f4062fb1311abe8c9ac707406c88 GIT binary patch literal 10636 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 ({ appId: item.app_id, created_at: item.created_at, updated_at: item.updated_at, - duration: - 500000 || - dayjs( - [EvaluationStatus.STARTED, EvaluationStatus.INITIALIZED].includes(item.status) - ? Date.now() - : item.updated_at, - ).diff(dayjs(item.created_at), "milliseconds"), + duration: calcEvalDuration(item), status: item.status, testset: { id: item.testset_id, @@ -103,7 +99,6 @@ const evaluationTransformer = (item: any) => ({ })), aggregated_results: item.aggregated_results || [], }) - export const fetchAllEvaluations = async (appId: string) => { const response = await axios.get(`/api/evaluations/`, {params: {app_id: appId}}) return response.data.map(evaluationTransformer) as _Evaluation[] From 15f4e78d8ea746f13396c974867c7bfd2f3b9673 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 18 Jan 2024 11:23:14 +0100 Subject: [PATCH 255/267] Update pyproject.toml --- agenta-cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-cli/pyproject.toml b/agenta-cli/pyproject.toml index ee2acb1777..f333754348 100644 --- a/agenta-cli/pyproject.toml +++ b/agenta-cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "agenta" -version = "0.7.1" +version = "0.8.0" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" authors = ["Mahmoud Mabrouk "] From 9bfd470270760daaf6e0f3d2e99d559c257d68bc Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Thu, 18 Jan 2024 12:42:01 +0100 Subject: [PATCH 256/267] move migrations to a separated folder --- agenta-backend/Dockerfile | 6 ++++++ .../20240110001454_initial_migration.py | 0 .../20240110132547_create_exact_match_evaluator_config.py | 0 .../20240110165900_evaluations_revamp.py | 0 .../20240112120721_evaluation_scenarios_revamp.py | 0 .../20240112120740_human_a_b_evaluation_scenarios.py | 0 ...0240112120800_human_single_model_evaluation_scenarios.py | 0 .../20240113131802_new_evaluation_results_aggregation.py | 0 .../20240113204909_change_odmantic_reference_to_link.py | 0 9 files changed, 6 insertions(+) rename agenta-backend/agenta_backend/migrations/{ => 17_01_24_pydantic_and_evaluations}/20240110001454_initial_migration.py (100%) rename agenta-backend/agenta_backend/migrations/{ => 17_01_24_pydantic_and_evaluations}/20240110132547_create_exact_match_evaluator_config.py (100%) rename agenta-backend/agenta_backend/migrations/{ => 17_01_24_pydantic_and_evaluations}/20240110165900_evaluations_revamp.py (100%) rename agenta-backend/agenta_backend/migrations/{ => 17_01_24_pydantic_and_evaluations}/20240112120721_evaluation_scenarios_revamp.py (100%) rename agenta-backend/agenta_backend/migrations/{ => 17_01_24_pydantic_and_evaluations}/20240112120740_human_a_b_evaluation_scenarios.py (100%) rename agenta-backend/agenta_backend/migrations/{ => 17_01_24_pydantic_and_evaluations}/20240112120800_human_single_model_evaluation_scenarios.py (100%) rename agenta-backend/agenta_backend/migrations/{ => 17_01_24_pydantic_and_evaluations}/20240113131802_new_evaluation_results_aggregation.py (100%) rename agenta-backend/agenta_backend/migrations/{ => 17_01_24_pydantic_and_evaluations}/20240113204909_change_odmantic_reference_to_link.py (100%) diff --git a/agenta-backend/Dockerfile b/agenta-backend/Dockerfile index db08614993..934d2eebcf 100644 --- a/agenta-backend/Dockerfile +++ b/agenta-backend/Dockerfile @@ -18,6 +18,12 @@ RUN touch /app/agenta_backend/__init__.py RUN poetry config virtualenvs.create false \ && poetry install --no-interaction --no-ansi +# Install git and clone the necessary repository +RUN apt-get update -y \ + && apt-get install -y git \ + && git clone https://github.com/mmabrouk/beanie \ + && cd beanie && pip install . + # remove dummy module RUN rm -r /app/agenta_backend EXPOSE 8000 \ No newline at end of file diff --git a/agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py b/agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240110001454_initial_migration.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240110001454_initial_migration.py rename to agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240110001454_initial_migration.py diff --git a/agenta-backend/agenta_backend/migrations/20240110132547_create_exact_match_evaluator_config.py b/agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240110132547_create_exact_match_evaluator_config.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240110132547_create_exact_match_evaluator_config.py rename to agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240110132547_create_exact_match_evaluator_config.py diff --git a/agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py b/agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240110165900_evaluations_revamp.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240110165900_evaluations_revamp.py rename to agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240110165900_evaluations_revamp.py diff --git a/agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py b/agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240112120721_evaluation_scenarios_revamp.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240112120721_evaluation_scenarios_revamp.py rename to agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240112120721_evaluation_scenarios_revamp.py diff --git a/agenta-backend/agenta_backend/migrations/20240112120740_human_a_b_evaluation_scenarios.py b/agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240112120740_human_a_b_evaluation_scenarios.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240112120740_human_a_b_evaluation_scenarios.py rename to agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240112120740_human_a_b_evaluation_scenarios.py diff --git a/agenta-backend/agenta_backend/migrations/20240112120800_human_single_model_evaluation_scenarios.py b/agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240112120800_human_single_model_evaluation_scenarios.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240112120800_human_single_model_evaluation_scenarios.py rename to agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240112120800_human_single_model_evaluation_scenarios.py diff --git a/agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py b/agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240113131802_new_evaluation_results_aggregation.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240113131802_new_evaluation_results_aggregation.py rename to agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240113131802_new_evaluation_results_aggregation.py diff --git a/agenta-backend/agenta_backend/migrations/20240113204909_change_odmantic_reference_to_link.py b/agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240113204909_change_odmantic_reference_to_link.py similarity index 100% rename from agenta-backend/agenta_backend/migrations/20240113204909_change_odmantic_reference_to_link.py rename to agenta-backend/agenta_backend/migrations/17_01_24_pydantic_and_evaluations/20240113204909_change_odmantic_reference_to_link.py From 308a584938927916e830980124fcd04e50d843fe Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Thu, 18 Jan 2024 12:51:42 +0100 Subject: [PATCH 257/267] add readme --- .../agenta_backend/migrations/README.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 agenta-backend/agenta_backend/migrations/README.md diff --git a/agenta-backend/agenta_backend/migrations/README.md b/agenta-backend/agenta_backend/migrations/README.md new file mode 100644 index 0000000000..fce2f5aba4 --- /dev/null +++ b/agenta-backend/agenta_backend/migrations/README.md @@ -0,0 +1,51 @@ +# Database Migrations + +This guide outlines the process for performing database migrations using Beanie with the Agenta backend system. + +Beanie is a MongoDB ODM (Object Document Mapper) for Python. More information about Beanie can be found [here](https://github.com/roman-right/beanie). + +## Steps for Migration + +### Accessing the Backend Docker Container + +To access the backend Docker container: + +1. **List Docker Containers**: List all running Docker containers with the command: + + ```bash + docker ps + ``` + +2. **Identify the `agenta-backend` Container ID**: Note down the container ID from the output. Example output: + + ``` + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + ae0c56933636 agenta-backend "uvicorn agenta_back…" 3 hours ago Up 3 hours 8000/tcp agenta-backend-1 + e35f6c8b7fcb agenta-agenta-web "docker-entrypoint.s…" 3 hours ago Up 3 hours 0.0.0.0:3000->3000/tcp agenta-agenta-web-1 + ``` + +3. **SSH into the Container**: Use the following command, replacing `CONTAINER_ID` with your container's ID: + + ```bash + docker exec -it CONTAINER_ID bash + ``` + +### Performing the Migration + +To perform the database migration: + +1. **Navigate to Migration Directory**: Change the directory to the migration folder: + + ```sh + cd agenta_backend/migrations/{migration_name} + ``` + Replace `{migration_name}` with the actual migration name, e.g., `17_01_24_pydantic_and_evaluations`. + +2. **Run Beanie Migration**: Execute the migration command: + + ```sh + beanie migrate --no-use-transaction -uri 'mongodb://username:password@mongo' -db 'agenta_v2' -p . + ``` + Ensure to replace `username`, `password`, and other placeholders with actual values. + +Follow these steps for a successful database migration in your Agenta backend system. \ No newline at end of file From 28b17b5f78546181cbd9bb154933be1d6e4d3f8e Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 18 Jan 2024 13:37:16 +0100 Subject: [PATCH 258/267] Added migration guide --- docs/mint.json | 1 + docs/self-host/migration.mdx | 50 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 docs/self-host/migration.mdx diff --git a/docs/mint.json b/docs/mint.json index c9e70ff99f..c7fa2536ef 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -100,6 +100,7 @@ "group": "Self-host agenta", "pages": [ "self-host/host-locally", + "self-host/migration", { "group": "Deploy Remotely", "pages": [ diff --git a/docs/self-host/migration.mdx b/docs/self-host/migration.mdx new file mode 100644 index 0000000000..41258214a3 --- /dev/null +++ b/docs/self-host/migration.mdx @@ -0,0 +1,50 @@ +--- +title: Migration +description: 'This is a step-by-step guide for upgrading to the latest version of Agenta' +--- + +## Upgrading to the Latest Version + +To upgrade to the latest version of Agenta, execute the following command: + +``` +docker compose -f docker-compose.gh.yml up -d --pull always + +``` + +This command instructs Docker to fetch and use the latest version of the Agenta image. + +## Database Migration with Beanie + +This guide offers detailed steps for performing database migrations in the Agenta backend system, using Beanie, a MongoDB ODM (Object Document Mapper) for Python. You can learn more about Beanie [here](https://github.com/roman-right/beanie). + +### Setting Up Beanie + +To install a custom version of Beanie: + +``` +git clone +cd beanie +pip install . + +``` + +### Migrating from Version 0.7 to 0.8 + +1. **Accessing the Migration Directory**: First, navigate to the migration directory: + + ``` + cd agenta_backend/migrations/17_01_24_pydantic_and_evaluations + + ``` + +2. **Executing the Beanie Migration**: Run the migration script as follows: + + ``` + beanie migrate --no-use-transaction -uri 'mongodb://username:password@localhost' -db 'agenta_v2' -p . + + ``` + + + Make sure to replace `username`, `password`, and other relevant placeholders with your actual credentials, unless you're using default settings. + \ No newline at end of file From d30e79071244c1d069cf45a9a02d7297bb4dca7a Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Thu, 18 Jan 2024 21:16:31 +0500 Subject: [PATCH 259/267] reverted dev.dockerFile code suppression --- agenta-web/dev.Dockerfile | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/agenta-web/dev.Dockerfile b/agenta-web/dev.Dockerfile index ad5bc9a53b..6af573852c 100644 --- a/agenta-web/dev.Dockerfile +++ b/agenta-web/dev.Dockerfile @@ -1,37 +1,37 @@ FROM node:18-alpine -# WORKDIR /app +WORKDIR /app -# # Install dependencies based on the preferred package manager -# COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ -# RUN \ -# if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ -# elif [ -f package-lock.json ]; then npm i; \ -# elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ -# # Allow install without lockfile, so example works even without Node.js installed locally -# else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ -# fi +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm i; \ + elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ + # Allow install without lockfile, so example works even without Node.js installed locally + else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \ + fi -# COPY src ./src -# COPY public ./public -# COPY next.config.js . -# COPY tsconfig.json . -# COPY postcss.config.js . -# COPY .env . -# RUN if [ -f .env.local ]; then cp .env.local .; fi -# # # used in cloud -# COPY sentry.* . -# # Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry -# # Uncomment the following line to disable telemetry at run time -# # ENV NEXT_TELEMETRY_DISABLED 1 +COPY src ./src +COPY public ./public +COPY next.config.js . +COPY tsconfig.json . +COPY postcss.config.js . +COPY .env . +RUN if [ -f .env.local ]; then cp .env.local .; fi +# # used in cloud +COPY sentry.* . +# Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry +# Uncomment the following line to disable telemetry at run time +# ENV NEXT_TELEMETRY_DISABLED 1 -# # Note: Don't expose ports here, Compose will handle that for us +# Note: Don't expose ports here, Compose will handle that for us -# # Start Next.js in development mode based on the preferred package manager -# CMD \ -# if [ -f yarn.lock ]; then yarn dev; \ -# elif [ -f package-lock.json ]; then npm run dev; \ -# elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ -# else yarn dev; \ -# fi +# Start Next.js in development mode based on the preferred package manager +CMD \ + if [ -f yarn.lock ]; then yarn dev; \ + elif [ -f package-lock.json ]; then npm run dev; \ + elif [ -f pnpm-lock.yaml ]; then pnpm dev; \ + else yarn dev; \ + fi From 89768985b23c33ffbc2fc5609d57b0bbb3c67a21 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 18 Jan 2024 19:20:12 +0100 Subject: [PATCH 260/267] Update pyproject.toml --- agenta-cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenta-cli/pyproject.toml b/agenta-cli/pyproject.toml index f333754348..030ea6c20f 100644 --- a/agenta-cli/pyproject.toml +++ b/agenta-cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "agenta" -version = "0.8.0" +version = "0.8.1" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" authors = ["Mahmoud Mabrouk "] From fc559944688a4a5d40ceab820d3b39aee171a55f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:26:53 +0000 Subject: [PATCH 261/267] Bump follow-redirects from 1.15.3 to 1.15.5 in /agenta-web Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.5. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.5) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- agenta-web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agenta-web/package-lock.json b/agenta-web/package-lock.json index 6f4ce2c284..178a4a62a8 100644 --- a/agenta-web/package-lock.json +++ b/agenta-web/package-lock.json @@ -4072,9 +4072,9 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", From c1be2303c2164257fe60fd01164533a1124cdf5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:27:38 +0000 Subject: [PATCH 262/267] Bump postcss from 8.4.23 to 8.4.31 in /agenta-web Bumps [postcss](https://github.com/postcss/postcss) from 8.4.23 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.23...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- agenta-web/package-lock.json | 8 ++++---- agenta-web/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/agenta-web/package-lock.json b/agenta-web/package-lock.json index 6f4ce2c284..43fc06656b 100644 --- a/agenta-web/package-lock.json +++ b/agenta-web/package-lock.json @@ -43,7 +43,7 @@ "next": "13.3.4", "nextjs-cors": "^2.1.2", "papaparse": "^5.4.1", - "postcss": "8.4.23", + "postcss": "8.4.31", "posthog-js": "^1.94.4", "promise-retry": "^2.0.1", "react": "18.2.0", @@ -7586,9 +7586,9 @@ } }, "node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", diff --git a/agenta-web/package.json b/agenta-web/package.json index 7f74a72908..19840202f0 100644 --- a/agenta-web/package.json +++ b/agenta-web/package.json @@ -54,7 +54,7 @@ "next": "13.3.4", "nextjs-cors": "^2.1.2", "papaparse": "^5.4.1", - "postcss": "8.4.23", + "postcss": "8.4.31", "posthog-js": "^1.94.4", "promise-retry": "^2.0.1", "react": "18.2.0", From 26881530c99b93a96c26f0d0c4d821eef17d4ae5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:28:00 +0000 Subject: [PATCH 263/267] Bump langchain from 0.0.256 to 0.0.329 in /agenta-backend Bumps [langchain](https://github.com/langchain-ai/langchain) from 0.0.256 to 0.0.329. - [Release notes](https://github.com/langchain-ai/langchain/releases) - [Commits](https://github.com/langchain-ai/langchain/compare/v0.0.256...v0.0.329) --- updated-dependencies: - dependency-name: langchain dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- agenta-backend/poetry.lock | 111 +++++++++++++--------------------- agenta-backend/pyproject.toml | 2 +- 2 files changed, 42 insertions(+), 71 deletions(-) diff --git a/agenta-backend/poetry.lock b/agenta-backend/poetry.lock index f64a6eca82..a954c4b140 100644 --- a/agenta-backend/poetry.lock +++ b/agenta-backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiodocker" @@ -1059,6 +1059,31 @@ files = [ {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, ] +[[package]] +name = "jsonpatch" +version = "1.33" +description = "Apply JSON-Patches (RFC 6902)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +files = [ + {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, + {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, +] + +[package.dependencies] +jsonpointer = ">=1.9" + +[[package]] +name = "jsonpointer" +version = "2.4" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +files = [ + {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, + {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, +] + [[package]] name = "kombu" version = "5.3.4" @@ -1120,40 +1145,41 @@ adal = ["adal (>=1.0.2)"] [[package]] name = "langchain" -version = "0.0.256" +version = "0.0.329" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain-0.0.256-py3-none-any.whl", hash = "sha256:3389fcb85d8d4fb16bae5ca9995d3ce634a3330f8ac1f458afc6171e4ca52de5"}, - {file = "langchain-0.0.256.tar.gz", hash = "sha256:b80115e19f86199c49bca8ef18c09d2d87548332a0144a1c5ce6a2f82e4f5f9c"}, + {file = "langchain-0.0.329-py3-none-any.whl", hash = "sha256:5f3e884991271e8b55eda4c63a11105dcd7da119682ce0e3d5d1385b3a4103d2"}, + {file = "langchain-0.0.329.tar.gz", hash = "sha256:488f3cb68a587696f136d4f01f97df8d8270e295b3cc56158057dab0f61f4166"}, ] [package.dependencies] aiohttp = ">=3.8.3,<4.0.0" +anyio = "<4.0" async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""} -dataclasses-json = ">=0.5.7,<0.6.0" -langsmith = ">=0.0.11,<0.1.0" -numexpr = ">=2.8.4,<3.0.0" +dataclasses-json = ">=0.5.7,<0.7" +jsonpatch = ">=1.33,<2.0" +langsmith = ">=0.0.52,<0.1.0" numpy = ">=1,<2" -openapi-schema-pydantic = ">=1.2,<2.0" -pydantic = ">=1,<2" +pydantic = ">=1,<3" PyYAML = ">=5.3" requests = ">=2,<3" SQLAlchemy = ">=1.4,<3" tenacity = ">=8.1.0,<9.0.0" [package.extras] -all = ["O365 (>=2.0.26,<3.0.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "amadeus (>=8.1.0)", "anthropic (>=0.3,<0.4)", "arxiv (>=1.4,<2.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "awadb (>=0.3.9,<0.4.0)", "azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "beautifulsoup4 (>=4,<5)", "clarifai (>=9.1.0)", "clickhouse-connect (>=0.5.14,<0.6.0)", "cohere (>=4,<5)", "deeplake (>=3.6.8,<4.0.0)", "docarray[hnswlib] (>=0.32.0,<0.33.0)", "duckduckgo-search (>=3.8.3,<4.0.0)", "elasticsearch (>=8,<9)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-auth (>=2.18.1,<3.0.0)", "google-search-results (>=2,<3)", "gptcache (>=0.1.7)", "html2text (>=2020.1.16,<2021.0.0)", "huggingface_hub (>=0,<1)", "jina (>=3.14,<4.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "lancedb (>=0.1,<0.2)", "langkit (>=0.0.6,<0.1.0)", "lark (>=1.1.5,<2.0.0)", "libdeeplake (>=0.0.60,<0.0.61)", "librosa (>=0.10.0.post2,<0.11.0)", "lxml (>=4.9.2,<5.0.0)", "manifest-ml (>=0.0.1,<0.0.2)", "marqo (>=0.11.0,<0.12.0)", "momento (>=1.5.0,<2.0.0)", "nebula3-python (>=3.4.0,<4.0.0)", "neo4j (>=5.8.1,<6.0.0)", "networkx (>=2.6.3,<3.0.0)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "octoai-sdk (>=0.1.1,<0.2.0)", "openai (>=0,<1)", "openlm (>=0.0.5,<0.0.6)", "opensearch-py (>=2.0.0,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pexpect (>=4.8.0,<5.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "pinecone-text (>=0.4.2,<0.5.0)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pymongo (>=4.3.3,<5.0.0)", "pyowm (>=3.3.0,<4.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pytesseract (>=0.3.10,<0.4.0)", "python-arango (>=7.5.9,<8.0.0)", "pyvespa (>=0.33.0,<0.34.0)", "qdrant-client (>=1.3.1,<2.0.0)", "rdflib (>=6.3.2,<7.0.0)", "redis (>=4,<5)", "requests-toolbelt (>=1.0.0,<2.0.0)", "sentence-transformers (>=2,<3)", "singlestoredb (>=0.7.1,<0.8.0)", "spacy (>=3,<4)", "steamship (>=2.16.9,<3.0.0)", "tensorflow-text (>=2.11.0,<3.0.0)", "tigrisdb (>=1.0.0b6,<2.0.0)", "tiktoken (>=0.3.2,<0.4.0)", "torch (>=1,<3)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)", "xinference (>=0.0.6,<0.0.7)"] -azure = ["azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-core (>=1.26.4,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "azure-search-documents (==11.4.0b6)", "openai (>=0,<1)"] +all = ["O365 (>=2.0.26,<3.0.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "amadeus (>=8.1.0)", "arxiv (>=1.4,<2.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "awadb (>=0.3.9,<0.4.0)", "azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "beautifulsoup4 (>=4,<5)", "clarifai (>=9.1.0)", "clickhouse-connect (>=0.5.14,<0.6.0)", "cohere (>=4,<5)", "deeplake (>=3.8.3,<4.0.0)", "docarray[hnswlib] (>=0.32.0,<0.33.0)", "duckduckgo-search (>=3.8.3,<4.0.0)", "elasticsearch (>=8,<9)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "google-api-python-client (==2.70.0)", "google-auth (>=2.18.1,<3.0.0)", "google-search-results (>=2,<3)", "gptcache (>=0.1.7)", "html2text (>=2020.1.16,<2021.0.0)", "huggingface_hub (>=0,<1)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "lancedb (>=0.1,<0.2)", "langkit (>=0.0.6,<0.1.0)", "lark (>=1.1.5,<2.0.0)", "librosa (>=0.10.0.post2,<0.11.0)", "lxml (>=4.9.2,<5.0.0)", "manifest-ml (>=0.0.1,<0.0.2)", "marqo (>=1.2.4,<2.0.0)", "momento (>=1.10.1,<2.0.0)", "nebula3-python (>=3.4.0,<4.0.0)", "neo4j (>=5.8.1,<6.0.0)", "networkx (>=2.6.3,<4)", "nlpcloud (>=1,<2)", "nltk (>=3,<4)", "nomic (>=1.0.43,<2.0.0)", "openai (>=0,<1)", "openlm (>=0.0.5,<0.0.6)", "opensearch-py (>=2.0.0,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pexpect (>=4.8.0,<5.0.0)", "pgvector (>=0.1.6,<0.2.0)", "pinecone-client (>=2,<3)", "pinecone-text (>=0.4.2,<0.5.0)", "psycopg2-binary (>=2.9.5,<3.0.0)", "pymongo (>=4.3.3,<5.0.0)", "pyowm (>=3.3.0,<4.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pytesseract (>=0.3.10,<0.4.0)", "python-arango (>=7.5.9,<8.0.0)", "pyvespa (>=0.33.0,<0.34.0)", "qdrant-client (>=1.3.1,<2.0.0)", "rdflib (>=6.3.2,<7.0.0)", "redis (>=4,<5)", "requests-toolbelt (>=1.0.0,<2.0.0)", "sentence-transformers (>=2,<3)", "singlestoredb (>=0.7.1,<0.8.0)", "tensorflow-text (>=2.11.0,<3.0.0)", "tigrisdb (>=1.0.0b6,<2.0.0)", "tiktoken (>=0.3.2,<0.6.0)", "torch (>=1,<3)", "transformers (>=4,<5)", "weaviate-client (>=3,<4)", "wikipedia (>=1,<2)", "wolframalpha (==5.0.0)"] +azure = ["azure-ai-formrecognizer (>=3.2.1,<4.0.0)", "azure-ai-vision (>=0.11.1b1,<0.12.0)", "azure-cognitiveservices-speech (>=1.28.0,<2.0.0)", "azure-core (>=1.26.4,<2.0.0)", "azure-cosmos (>=4.4.0b1,<5.0.0)", "azure-identity (>=1.12.0,<2.0.0)", "azure-search-documents (==11.4.0b8)", "openai (>=0,<1)"] clarifai = ["clarifai (>=9.1.0)"] +cli = ["typer (>=0.9.0,<0.10.0)"] cohere = ["cohere (>=4,<5)"] docarray = ["docarray[hnswlib] (>=0.32.0,<0.33.0)"] embeddings = ["sentence-transformers (>=2,<3)"] -extended-testing = ["amazon-textract-caller (<2)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.0.7,<0.0.8)", "chardet (>=5.1.0,<6.0.0)", "esprima (>=4.0.1,<5.0.0)", "feedparser (>=6.0.10,<7.0.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "gql (>=3.4.1,<4.0.0)", "html2text (>=2020.1.16,<2021.0.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "lxml (>=4.9.2,<5.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "openai (>=0,<1)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "tqdm (>=4.48.0)", "xata (>=1.0.0a7,<2.0.0)", "xinference (>=0.0.6,<0.0.7)", "zep-python (>=0.32)"] +extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "dashvector (>=1.0.1,<2.0.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.6.0,<0.7.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "html2text (>=2020.1.16,<2021.0.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (>=0,<1)", "openapi-pydantic (>=0.3.2,<0.4.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"] javascript = ["esprima (>=4.0.1,<5.0.0)"] -llms = ["anthropic (>=0.3,<0.4)", "clarifai (>=9.1.0)", "cohere (>=4,<5)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (>=0,<1)", "openllm (>=0.1.19)", "openlm (>=0.0.5,<0.0.6)", "torch (>=1,<3)", "transformers (>=4,<5)", "xinference (>=0.0.6,<0.0.7)"] -openai = ["openai (>=0,<1)", "tiktoken (>=0.3.2,<0.4.0)"] +llms = ["clarifai (>=9.1.0)", "cohere (>=4,<5)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (>=0,<1)", "openlm (>=0.0.5,<0.0.6)", "torch (>=1,<3)", "transformers (>=4,<5)"] +openai = ["openai (>=0,<1)", "tiktoken (>=0.3.2,<0.6.0)"] qdrant = ["qdrant-client (>=1.3.1,<2.0.0)"] text-helpers = ["chardet (>=5.1.0,<6.0.0)"] @@ -1337,47 +1363,6 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] -[[package]] -name = "numexpr" -version = "2.8.7" -description = "Fast numerical expression evaluator for NumPy" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numexpr-2.8.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d88531ffea3ea9287e8a1665c6a2d0206d3f4660d5244423e2a134a7f0ce5fba"}, - {file = "numexpr-2.8.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db1065ba663a854115cf1f493afd7206e2efcef6643129e8061e97a51ad66ebb"}, - {file = "numexpr-2.8.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4546416004ff2e7eb9cf52c2d7ab82732b1b505593193ee9f93fa770edc5230"}, - {file = "numexpr-2.8.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb2f473fdfd09d17db3038e34818d05b6bc561a36785aa927d6c0e06bccc9911"}, - {file = "numexpr-2.8.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5496fc9e3ae214637cbca1ab556b0e602bd3afe9ff4c943a29c482430972cda8"}, - {file = "numexpr-2.8.7-cp310-cp310-win32.whl", hash = "sha256:d43f1f0253a6f2db2f76214e6f7ae9611b422cba3f7d4c86415d7a78bbbd606f"}, - {file = "numexpr-2.8.7-cp310-cp310-win_amd64.whl", hash = "sha256:cf5f112bce5c5966c47cc33700bc14ce745c8351d437ed57a9574fff581f341a"}, - {file = "numexpr-2.8.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32934d51b5bc8a6636436326da79ed380e2f151989968789cf65b1210572cb46"}, - {file = "numexpr-2.8.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f021ac93cb3dd5d8ba2882627b615b1f58cb089dcc85764c6fbe7a549ed21b0c"}, - {file = "numexpr-2.8.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dccf572763517db6562fb7b17db46aacbbf62a9ca0a66672872f4f71aee7b186"}, - {file = "numexpr-2.8.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11121b14ee3179bade92e823f25f1b94e18716d33845db5081973331188c3338"}, - {file = "numexpr-2.8.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:81451962d4145a46dba189df65df101d4d1caddb6efe6ebfe05982cd9f62b2cf"}, - {file = "numexpr-2.8.7-cp311-cp311-win32.whl", hash = "sha256:da55ba845b847cc33c4bf81cee4b1bddfb0831118cabff8db62888ab8697ec34"}, - {file = "numexpr-2.8.7-cp311-cp311-win_amd64.whl", hash = "sha256:fd93b88d5332069916fa00829ea1b972b7e73abcb1081eee5c905a514b8b59e3"}, - {file = "numexpr-2.8.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5340d2c86d83f52e1a3e7fd97c37d358ae99af9de316bdeeab2565b9b1e622ca"}, - {file = "numexpr-2.8.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3bdf8cbc00c77a46230c765d242f92d35905c239b20c256c48dbac91e49f253"}, - {file = "numexpr-2.8.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d46c47e361fa60966a3339cb4f463ae6151ce7d78ed38075f06e8585d2c8929f"}, - {file = "numexpr-2.8.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a371cfc1670a18eea2d5c70abaa95a0e8824b70d28da884bad11931266e3a0ca"}, - {file = "numexpr-2.8.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:47a249cecd1382d482a5bf1fac0d11392fb2ed0f7d415ebc4cd901959deb1ec9"}, - {file = "numexpr-2.8.7-cp312-cp312-win32.whl", hash = "sha256:b8a5b2c21c26b62875bf819d375d798b96a32644e3c28bd4ce7789ed1fb489da"}, - {file = "numexpr-2.8.7-cp312-cp312-win_amd64.whl", hash = "sha256:f29f4d08d9b0ed6fa5d32082971294b2f9131b8577c2b7c36432ed670924313f"}, - {file = "numexpr-2.8.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ecaa5be24cf8fa0f00108e9dfa1021b7510e9dd9d159b8d8bc7c7ddbb995b31"}, - {file = "numexpr-2.8.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a84284e0a407ca52980fd20962e89aff671c84cd6e73458f2e29ea2aa206356"}, - {file = "numexpr-2.8.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e838289e3b7bbe100b99e35496e6cc4cc0541c2207078941ee5a1d46e6b925ae"}, - {file = "numexpr-2.8.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0983052f308ea75dd232eb7f4729eed839db8fe8d82289940342b32cc55b15d0"}, - {file = "numexpr-2.8.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8bf005acd7f1985c71b1b247aaac8950d6ea05a0fe0bbbbf3f96cd398b136daa"}, - {file = "numexpr-2.8.7-cp39-cp39-win32.whl", hash = "sha256:56ec95f8d1db0819e64987dcf1789acd500fa4ea396eeabe4af6efdcb8902d07"}, - {file = "numexpr-2.8.7-cp39-cp39-win_amd64.whl", hash = "sha256:c7bf60fc1a9c90a9cb21c4c235723e579bff70c8d5362228cb2cf34426104ba2"}, - {file = "numexpr-2.8.7.tar.gz", hash = "sha256:596eeb3bbfebc912f4b6eaaf842b61ba722cebdb8bc42dfefa657d3a74953849"}, -] - -[package.dependencies] -numpy = ">=1.13.3" - [[package]] name = "numpy" version = "1.26.2" @@ -1484,20 +1469,6 @@ dev = ["black (>=21.6b0,<22.0)", "pytest (==6.*)", "pytest-asyncio", "pytest-moc embeddings = ["matplotlib", "numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)", "plotly", "scikit-learn (>=1.0.2)", "scipy", "tenacity (>=8.0.1)"] wandb = ["numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)", "wandb"] -[[package]] -name = "openapi-schema-pydantic" -version = "1.2.4" -description = "OpenAPI (v3) specification schema as pydantic class" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "openapi-schema-pydantic-1.2.4.tar.gz", hash = "sha256:3e22cf58b74a69f752cc7e5f1537f6e44164282db2700cbbcd3bb99ddd065196"}, - {file = "openapi_schema_pydantic-1.2.4-py3-none-any.whl", hash = "sha256:a932ecc5dcbb308950282088956e94dea069c9823c84e507d64f6b622222098c"}, -] - -[package.dependencies] -pydantic = ">=1.8.2" - [[package]] name = "packaging" version = "23.2" @@ -2771,4 +2742,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "007f83104a7bff7addf4a6e982d210a74152ae446065a179e85000842c4a471e" +content-hash = "3e56ee6faaabfce258cd4e14ba22a97bc24336a120e20d5df94342be7fb47427" diff --git a/agenta-backend/pyproject.toml b/agenta-backend/pyproject.toml index d14f74b1e1..392395de7a 100644 --- a/agenta-backend/pyproject.toml +++ b/agenta-backend/pyproject.toml @@ -20,7 +20,7 @@ backoff = "^2.2.1" redis = "^4.6.0" aiodocker = "^0.21.0" openai = "^0.27.8" -langchain = "^0.0.256" +langchain = "^0.0.329" odmantic = "^0.9.2" supertokens-python = "^0.15.1" sendgrid = "^6.10.0" From 6c3f57ba1b837f8a7aaa6c12bb1b146260ba301b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:29:02 +0000 Subject: [PATCH 264/267] Bump cryptography from 41.0.5 to 41.0.6 in /agenta-backend Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.5 to 41.0.6. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/41.0.5...41.0.6) --- updated-dependencies: - dependency-name: cryptography dependency-type: indirect ... Signed-off-by: dependabot[bot] --- agenta-backend/poetry.lock | 50 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/agenta-backend/poetry.lock b/agenta-backend/poetry.lock index f64a6eca82..f1898f054e 100644 --- a/agenta-backend/poetry.lock +++ b/agenta-backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiodocker" @@ -638,34 +638,34 @@ files = [ [[package]] name = "cryptography" -version = "41.0.5" +version = "41.0.6" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, - {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, - {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, - {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, + {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c"}, + {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d"}, + {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c"}, + {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596"}, + {file = "cryptography-41.0.6-cp37-abi3-win32.whl", hash = "sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660"}, + {file = "cryptography-41.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4"}, + {file = "cryptography-41.0.6.tar.gz", hash = "sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3"}, ] [package.dependencies] From e681646ba3b8e28a22e119754796768d5a5fe45b Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Thu, 18 Jan 2024 19:37:54 +0100 Subject: [PATCH 265/267] Create SECURITY.md --- SECURITY.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..f45e7a7624 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy +## Reporting a Vulnerability + +If you believe you have found a security vulnerability in any Agenta repository, please report it to us through coordinated disclosure. + +Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests. + +Instead, please send an email to team@agenta.ai. + +Please include as much of the information listed below as you can to help us better understand and resolve the issue: + + The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) + Full paths of source file(s) related to the manifestation of the issue + The location of the affected source code (tag/branch/commit or direct URL) + Any special configuration required to reproduce the issue + Step-by-step instructions to reproduce the issue + Proof-of-concept or exploit code (if possible) + Impact of the issue, including how an attacker might exploit the issue + From dc91eac9a3278f6c6f083afb3fcc8d71fc568d31 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Thu, 18 Jan 2024 20:46:36 +0100 Subject: [PATCH 266/267] modified human eval column name --- agenta-web/cypress/tsconfig.json | 4 +- .../ABTestingEvaluationTable.tsx | 53 +++++++++---------- .../SingleModelEvaluationTable.tsx | 53 +++++++++---------- agenta-web/src/lib/helpers/evaluate.ts | 4 +- agenta-web/tsconfig.json | 6 +-- 5 files changed, 55 insertions(+), 65 deletions(-) diff --git a/agenta-web/cypress/tsconfig.json b/agenta-web/cypress/tsconfig.json index dc618360a0..1235d8c20f 100644 --- a/agenta-web/cypress/tsconfig.json +++ b/agenta-web/cypress/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es5", "lib": ["es5", "dom"], - "types": ["cypress", "node"] + "types": ["cypress", "node"], }, - "include": ["**/*.ts"] + "include": ["**/*.ts"], } diff --git a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx index 45de721c9a..e05d6b8965 100644 --- a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx @@ -381,10 +381,32 @@ const ABTestingEvaluationTable: React.FC = ({ }, }, { - key: "correctAnswer", title: "Expected Output", - dataIndex: "correctAnswer", + dataIndex: "expectedOutput", + key: "expectedOutput", width: "25%", + render: (text: any, record: any, rowIndex: number) => { + let correctAnswer = + record.correctAnswer || evaluation.testset.csvdata[rowIndex].correct_answer + + return ( + <> + + depouncedUpdateEvaluationScenario( + { + correctAnswer: e.target.value, + }, + record.id, + ) + } + key={record.id} + /> + + ) + }, }, ...dynamicColumns, { @@ -409,33 +431,6 @@ const ABTestingEvaluationTable: React.FC = ({ ) }, }, - { - title: "Expected Answer", - dataIndex: "expectedAnswer", - key: "expectedAnswer", - render: (text: any, record: any, rowIndex: number) => { - let correctAnswer = - record.correctAnswer || evaluation.testset.csvdata[rowIndex].correct_answer - - return ( - <> - - depouncedUpdateEvaluationScenario( - { - correctAnswer: e.target.value, - }, - record.id, - ) - } - key={record.id} - /> - - ) - }, - }, { title: "Additional Note", dataIndex: "additionalNote", diff --git a/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx index 0c92498e37..ae12c869fe 100644 --- a/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx +++ b/agenta-web/src/components/EvaluationTable/SingleModelEvaluationTable.tsx @@ -427,10 +427,32 @@ const SingleModelEvaluationTable: React.FC = ({ }, }, { - key: "correctAnswer", title: "Expected Output", - dataIndex: "correctAnswer", + dataIndex: "expectedOutput", + key: "expectedOutput", width: "25%", + render: (text: any, record: any, rowIndex: number) => { + let correctAnswer = + record.correctAnswer || evaluation.testset.csvdata[rowIndex].correct_answer + + return ( + <> + + depouncedUpdateEvaluationScenario( + { + correctAnswer: e.target.value, + }, + record.id, + ) + } + key={record.id} + /> + + ) + }, }, ...dynamicColumns, { @@ -462,33 +484,6 @@ const SingleModelEvaluationTable: React.FC = ({ ) }, }, - { - title: "Expected Answer", - dataIndex: "expectedAnswer", - key: "expectedAnswer", - render: (text: any, record: any, rowIndex: number) => { - let correctAnswer = - record.correctAnswer || evaluation.testset.csvdata[rowIndex].correct_answer - - return ( - <> - - depouncedUpdateEvaluationScenario( - { - correctAnswer: e.target.value, - }, - record.id, - ) - } - key={record.id} - /> - - ) - }, - }, { title: "Additional Note", dataIndex: "additionalNote", diff --git a/agenta-web/src/lib/helpers/evaluate.ts b/agenta-web/src/lib/helpers/evaluate.ts index d5902915f9..934ee400d1 100644 --- a/agenta-web/src/lib/helpers/evaluate.ts +++ b/agenta-web/src/lib/helpers/evaluate.ts @@ -99,7 +99,7 @@ export const exportABTestingEvaluationData = ( ["Vote"]: evaluation.variants.find((v: Variant) => v.variantId === data.vote)?.variantName || data.vote, - ["Expected answer"]: + ["Expected Output"]: scenarios[ix]?.correctAnswer || evaluation.testset.csvdata[ix].correct_answer, ["Additional notes"]: scenarios[ix]?.note, } @@ -133,7 +133,7 @@ export const exportSingleModelEvaluationData = ( ? data?.columnData0 : data.outputs[0]?.variant_output, ["Score"]: isNaN(numericScore) ? "-" : numericScore, - ["Expected answer"]: + ["Expected Output"]: scenarios[ix]?.correctAnswer || evaluation.testset.csvdata[ix].correct_answer, ["Additional notes"]: scenarios[ix]?.note, } diff --git a/agenta-web/tsconfig.json b/agenta-web/tsconfig.json index 73b324e040..db62be51b5 100644 --- a/agenta-web/tsconfig.json +++ b/agenta-web/tsconfig.json @@ -15,9 +15,9 @@ "jsx": "preserve", "incremental": true, "paths": { - "@/*": ["./src/*"] - } + "@/*": ["./src/*"], + }, }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "cypress.config.ts", "cypress/**/*"] + "exclude": ["node_modules", "cypress.config.ts", "cypress/**/*"], } From a7f8be59ab90494683c31396b8f35b422b792f46 Mon Sep 17 00:00:00 2001 From: Akrem Abayed Date: Fri, 19 Jan 2024 17:30:23 +0100 Subject: [PATCH 267/267] fix ai critique --- .../services/evaluators_service.py | 6 +-- .../agenta_backend/tasks/evaluations.py | 45 +++++++++++-------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/agenta-backend/agenta_backend/services/evaluators_service.py b/agenta-backend/agenta_backend/services/evaluators_service.py index 15c1d7832f..372f2172f7 100644 --- a/agenta-backend/agenta_backend/services/evaluators_service.py +++ b/agenta-backend/agenta_backend/services/evaluators_service.py @@ -150,10 +150,8 @@ def auto_ai_critique( "correct_answer": correct_answer, } - for input_item in app_params.get("inputs", []): - input_name = input_item.get("name") - if input_name and input_name in inputs: - chain_run_args[input_name] = inputs[input_name] + for key, value in inputs.items(): + chain_run_args[key] = value prompt = PromptTemplate( input_variables=list(chain_run_args.keys()), # Use the keys from chain_run_args diff --git a/agenta-backend/agenta_backend/tasks/evaluations.py b/agenta-backend/agenta_backend/tasks/evaluations.py index 6bb45e0924..dea2ee5a42 100644 --- a/agenta-backend/agenta_backend/tasks/evaluations.py +++ b/agenta-backend/agenta_backend/tasks/evaluations.py @@ -1,6 +1,7 @@ import asyncio import logging import os +import re import traceback from collections import defaultdict from typing import Any, Dict, List @@ -208,28 +209,34 @@ async def aggregate_evaluator_results( for config_id, val in evaluators_aggregated_data.items(): evaluator_key = val["evaluator_key"] or "" results = val["results"] or [] - if evaluator_key != "auto_ai_critique": + + if not results: + average_value = 0 + if evaluator_key == "auto_ai_critique": + numeric_scores = [] + for result in results: + # Extract the first number found in the result value + match = re.search(r"\d+", result.value) + if match: + try: + score = int(match.group()) + numeric_scores.append(score) + except ValueError: + # Ignore if the extracted value is not an integer + continue + + # Calculate the average of numeric scores if any are present average_value = ( - sum([result.value for result in results]) / len(results) - if results - else 0 + sum(numeric_scores) / len(numeric_scores) if numeric_scores else None ) - elif evaluator_key == "auto_ai_critique": - try: - average_value = ( - sum( - [ - int(result.value) - for result in results - if isinstance(int(result.value), int) - ] - ) - / len(results) - if results - else 0 - ) - except TypeError: + else: + # Handle boolean values for auto_regex_test and other evaluators + if all(isinstance(result.value, bool) for result in results): + average_value = sum(result.value for result in results) / len(results) + else: + # Handle other data types or mixed results average_value = None + evaluator_config = await fetch_evaluator_config(config_id) aggregated_result = AggregatedResult( evaluator_config=evaluator_config.id,