From 8be6ab65bec27b738993a8eb5edd89a99c40fa24 Mon Sep 17 00:00:00 2001 From: Abram Date: Sun, 10 Dec 2023 14:24:58 +0100 Subject: [PATCH 001/156] 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/156] 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/156] 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/156] :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/156] 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/156] 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/156] 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/156] 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/156] 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/156] 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/156] 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/156] 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/156] 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/156] 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/156] :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/156] :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/156] 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/156] 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 092/156] 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 093/156] 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 094/156] 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 095/156] 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 096/156] 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 097/156] 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 098/156] 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 099/156] :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 100/156] 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 101/156] 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 102/156] 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 103/156] 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 104/156] 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 105/156] 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 106/156] 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 107/156] 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 108/156] 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 109/156] 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 110/156] 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 111/156] 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 112/156] 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 113/156] 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 114/156] 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 115/156] 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 116/156] 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 117/156] 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 118/156] 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 119/156] 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 120/156] 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 121/156] 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 122/156] 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 123/156] 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 124/156] 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 125/156] 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 126/156] 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 127/156] 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 128/156] 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 129/156] 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 130/156] 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 131/156] 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 132/156] 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 133/156] 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 134/156] 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 135/156] 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 136/156] 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 137/156] 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 138/156] 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 139/156] 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 140/156] 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 141/156] 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 142/156] 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 143/156] 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 144/156] 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 39862c2f026c7dced675ee192a55eac2b10fe929 Mon Sep 17 00:00:00 2001 From: Kaosiso Ezealigo Date: Mon, 8 Jan 2024 00:16:01 +0100 Subject: [PATCH 145/156] added null checks --- agenta-web/src/components/Playground/Views/TestView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agenta-web/src/components/Playground/Views/TestView.tsx b/agenta-web/src/components/Playground/Views/TestView.tsx index f224a3d98c..fd61056d37 100644 --- a/agenta-web/src/components/Playground/Views/TestView.tsx +++ b/agenta-web/src/components/Playground/Views/TestView.tsx @@ -200,12 +200,12 @@ const BoxComponent: React.FC = ({ placeholder="Results will be shown here" disabled={!result || result === LOADING_TEXT} style={{ - background: result.startsWith("โŒ Error code") + background: result?.startsWith("โŒ Error code") ? appTheme === "dark" ? "#490b0b" : "#fff1f0" : "", - color: result.startsWith("โŒ Error code") + color: result?.startsWith("โŒ Error code") ? appTheme === "dark" ? "#ffffffd9" : "#000000e0" From 10f757939382732c2147d4f92fa081e9e9729f7b Mon Sep 17 00:00:00 2001 From: Nehemiah Onyekachukwu Emmanuel Date: Mon, 8 Jan 2024 09:33:54 +0100 Subject: [PATCH 146/156] 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 147/156] 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 148/156] 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 149/156] 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 150/156] 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 151/156] 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 152/156] 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 153/156] 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 154/156] 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 155/156] 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 156/156] 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)