mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-09 08:08:32 +00:00
Merge branch 'main' into gl/chore/use-base-model-for-llms
This commit is contained in:
@@ -12,12 +12,16 @@ from pydantic.types import ImportString
|
|||||||
|
|
||||||
|
|
||||||
class QdrantToolSchema(BaseModel):
|
class QdrantToolSchema(BaseModel):
|
||||||
query: str = Field(..., description="Query to search in Qdrant DB")
|
query: str = Field(
|
||||||
|
..., description="Query to search in Qdrant DB - always required."
|
||||||
|
)
|
||||||
filter_by: str | None = Field(
|
filter_by: str | None = Field(
|
||||||
default=None, description="Parameter to filter the search by."
|
default=None,
|
||||||
|
description="Parameter to filter the search by. When filtering, needs to be used in conjunction with filter_value.",
|
||||||
)
|
)
|
||||||
filter_value: Any | None = Field(
|
filter_value: Any | None = Field(
|
||||||
default=None, description="Value to filter the search by."
|
default=None,
|
||||||
|
description="Value to filter the search by. When filtering, needs to be used in conjunction with filter_by.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ from crewai.utilities.agent_utils import (
|
|||||||
)
|
)
|
||||||
from crewai.utilities.constants import TRAINING_DATA_FILE
|
from crewai.utilities.constants import TRAINING_DATA_FILE
|
||||||
from crewai.utilities.i18n import I18N, get_i18n
|
from crewai.utilities.i18n import I18N, get_i18n
|
||||||
|
from crewai.utilities.llm_call_hooks import (
|
||||||
|
get_after_llm_call_hooks,
|
||||||
|
get_before_llm_call_hooks,
|
||||||
|
)
|
||||||
from crewai.utilities.printer import Printer
|
from crewai.utilities.printer import Printer
|
||||||
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
||||||
from crewai.utilities.training_handler import CrewTrainingHandler
|
from crewai.utilities.training_handler import CrewTrainingHandler
|
||||||
@@ -130,6 +134,10 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
|||||||
self.messages: list[LLMMessage] = []
|
self.messages: list[LLMMessage] = []
|
||||||
self.iterations = 0
|
self.iterations = 0
|
||||||
self.log_error_after = 3
|
self.log_error_after = 3
|
||||||
|
self.before_llm_call_hooks: list[Callable] = []
|
||||||
|
self.after_llm_call_hooks: list[Callable] = []
|
||||||
|
self.before_llm_call_hooks.extend(get_before_llm_call_hooks())
|
||||||
|
self.after_llm_call_hooks.extend(get_after_llm_call_hooks())
|
||||||
if self.llm:
|
if self.llm:
|
||||||
# This may be mutating the shared llm object and needs further evaluation
|
# This may be mutating the shared llm object and needs further evaluation
|
||||||
existing_stop = getattr(self.llm, "stop", [])
|
existing_stop = getattr(self.llm, "stop", [])
|
||||||
@@ -226,6 +234,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
|||||||
from_task=self.task,
|
from_task=self.task,
|
||||||
from_agent=self.agent,
|
from_agent=self.agent,
|
||||||
response_model=self.response_model,
|
response_model=self.response_model,
|
||||||
|
executor_context=self,
|
||||||
)
|
)
|
||||||
formatted_answer = process_llm_response(answer, self.use_stop_words) # type: ignore[assignment]
|
formatted_answer = process_llm_response(answer, self.use_stop_words) # type: ignore[assignment]
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -13,6 +13,8 @@ from crewai.cli.shared.token_manager import TokenManager
|
|||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
TOauth2Settings = TypeVar("TOauth2Settings", bound="Oauth2Settings")
|
||||||
|
|
||||||
|
|
||||||
class Oauth2Settings(BaseModel):
|
class Oauth2Settings(BaseModel):
|
||||||
provider: str = Field(
|
provider: str = Field(
|
||||||
@@ -28,9 +30,15 @@ class Oauth2Settings(BaseModel):
|
|||||||
description="OAuth2 audience value, typically used to identify the target API or resource.",
|
description="OAuth2 audience value, typically used to identify the target API or resource.",
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
extra: dict[str, Any] = Field(
|
||||||
|
description="Extra configuration for the OAuth2 provider.",
|
||||||
|
default={},
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_settings(cls):
|
def from_settings(cls: type[TOauth2Settings]) -> TOauth2Settings:
|
||||||
|
"""Create an Oauth2Settings instance from the CLI settings."""
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
@@ -38,12 +46,20 @@ class Oauth2Settings(BaseModel):
|
|||||||
domain=settings.oauth2_domain,
|
domain=settings.oauth2_domain,
|
||||||
client_id=settings.oauth2_client_id,
|
client_id=settings.oauth2_client_id,
|
||||||
audience=settings.oauth2_audience,
|
audience=settings.oauth2_audience,
|
||||||
|
extra=settings.oauth2_extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from crewai.cli.authentication.providers.base_provider import BaseProvider
|
||||||
|
|
||||||
|
|
||||||
class ProviderFactory:
|
class ProviderFactory:
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_settings(cls, settings: Oauth2Settings | None = None):
|
def from_settings(
|
||||||
|
cls: type["ProviderFactory"], # noqa: UP037
|
||||||
|
settings: Oauth2Settings | None = None,
|
||||||
|
) -> "BaseProvider": # noqa: UP037
|
||||||
settings = settings or Oauth2Settings.from_settings()
|
settings = settings or Oauth2Settings.from_settings()
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
@@ -53,11 +69,11 @@ class ProviderFactory:
|
|||||||
)
|
)
|
||||||
provider = getattr(module, f"{settings.provider.capitalize()}Provider")
|
provider = getattr(module, f"{settings.provider.capitalize()}Provider")
|
||||||
|
|
||||||
return provider(settings)
|
return cast("BaseProvider", provider(settings))
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationCommand:
|
class AuthenticationCommand:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.token_manager = TokenManager()
|
self.token_manager = TokenManager()
|
||||||
self.oauth2_provider = ProviderFactory.from_settings()
|
self.oauth2_provider = ProviderFactory.from_settings()
|
||||||
|
|
||||||
@@ -84,7 +100,7 @@ class AuthenticationCommand:
|
|||||||
timeout=20,
|
timeout=20,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return cast(dict[str, Any], response.json())
|
||||||
|
|
||||||
def _display_auth_instructions(self, device_code_data: dict[str, str]) -> None:
|
def _display_auth_instructions(self, device_code_data: dict[str, str]) -> None:
|
||||||
"""Display the authentication instructions to the user."""
|
"""Display the authentication instructions to the user."""
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ class BaseProvider(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_client_id(self) -> str: ...
|
def get_client_id(self) -> str: ...
|
||||||
|
|
||||||
|
def get_required_fields(self) -> list[str]:
|
||||||
|
"""Returns which provider-specific fields inside the "extra" dict will be required"""
|
||||||
|
return []
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ from crewai.cli.authentication.providers.base_provider import BaseProvider
|
|||||||
|
|
||||||
class OktaProvider(BaseProvider):
|
class OktaProvider(BaseProvider):
|
||||||
def get_authorize_url(self) -> str:
|
def get_authorize_url(self) -> str:
|
||||||
return f"https://{self.settings.domain}/oauth2/default/v1/device/authorize"
|
return f"{self._oauth2_base_url()}/v1/device/authorize"
|
||||||
|
|
||||||
def get_token_url(self) -> str:
|
def get_token_url(self) -> str:
|
||||||
return f"https://{self.settings.domain}/oauth2/default/v1/token"
|
return f"{self._oauth2_base_url()}/v1/token"
|
||||||
|
|
||||||
def get_jwks_url(self) -> str:
|
def get_jwks_url(self) -> str:
|
||||||
return f"https://{self.settings.domain}/oauth2/default/v1/keys"
|
return f"{self._oauth2_base_url()}/v1/keys"
|
||||||
|
|
||||||
def get_issuer(self) -> str:
|
def get_issuer(self) -> str:
|
||||||
return f"https://{self.settings.domain}/oauth2/default"
|
return self._oauth2_base_url().removesuffix("/oauth2")
|
||||||
|
|
||||||
def get_audience(self) -> str:
|
def get_audience(self) -> str:
|
||||||
if self.settings.audience is None:
|
if self.settings.audience is None:
|
||||||
@@ -27,3 +27,16 @@ class OktaProvider(BaseProvider):
|
|||||||
"Client ID is required. Please set it in the configuration."
|
"Client ID is required. Please set it in the configuration."
|
||||||
)
|
)
|
||||||
return self.settings.client_id
|
return self.settings.client_id
|
||||||
|
|
||||||
|
def get_required_fields(self) -> list[str]:
|
||||||
|
return ["authorization_server_name", "using_org_auth_server"]
|
||||||
|
|
||||||
|
def _oauth2_base_url(self) -> str:
|
||||||
|
using_org_auth_server = self.settings.extra.get("using_org_auth_server", False)
|
||||||
|
|
||||||
|
if using_org_auth_server:
|
||||||
|
base_url = f"https://{self.settings.domain}/oauth2"
|
||||||
|
else:
|
||||||
|
base_url = f"https://{self.settings.domain}/oauth2/{self.settings.extra.get('authorization_server_name', 'default')}"
|
||||||
|
|
||||||
|
return f"{base_url}"
|
||||||
|
|||||||
@@ -11,18 +11,18 @@ console = Console()
|
|||||||
|
|
||||||
|
|
||||||
class BaseCommand:
|
class BaseCommand:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self._telemetry = Telemetry()
|
self._telemetry = Telemetry()
|
||||||
self._telemetry.set_tracer()
|
self._telemetry.set_tracer()
|
||||||
|
|
||||||
|
|
||||||
class PlusAPIMixin:
|
class PlusAPIMixin:
|
||||||
def __init__(self, telemetry):
|
def __init__(self, telemetry: Telemetry) -> None:
|
||||||
try:
|
try:
|
||||||
telemetry.set_tracer()
|
telemetry.set_tracer()
|
||||||
self.plus_api_client = PlusAPI(api_key=get_auth_token())
|
self.plus_api_client = PlusAPI(api_key=get_auth_token())
|
||||||
except Exception:
|
except Exception:
|
||||||
self._deploy_signup_error_span = telemetry.deploy_signup_error_span()
|
telemetry.deploy_signup_error_span()
|
||||||
console.print(
|
console.print(
|
||||||
"Please sign up/login to CrewAI+ before using the CLI.",
|
"Please sign up/login to CrewAI+ before using the CLI.",
|
||||||
style="bold red",
|
style="bold red",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import json
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -136,7 +137,12 @@ class Settings(BaseModel):
|
|||||||
default=DEFAULT_CLI_SETTINGS["oauth2_domain"],
|
default=DEFAULT_CLI_SETTINGS["oauth2_domain"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, config_path: Path | None = None, **data):
|
oauth2_extra: dict[str, Any] = Field(
|
||||||
|
description="Extra configuration for the OAuth2 provider.",
|
||||||
|
default={},
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, config_path: Path | None = None, **data: dict[str, Any]) -> None:
|
||||||
"""Load Settings from config path with fallback support"""
|
"""Load Settings from config path with fallback support"""
|
||||||
if config_path is None:
|
if config_path is None:
|
||||||
config_path = get_writable_config_path()
|
config_path = get_writable_config_path()
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.exceptions import JSONDecodeError, RequestException
|
from requests.exceptions import JSONDecodeError, RequestException
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
|
from crewai.cli.authentication.main import Oauth2Settings, ProviderFactory
|
||||||
from crewai.cli.command import BaseCommand
|
from crewai.cli.command import BaseCommand
|
||||||
from crewai.cli.settings.main import SettingsCommand
|
from crewai.cli.settings.main import SettingsCommand
|
||||||
from crewai.cli.version import get_crewai_version
|
from crewai.cli.version import get_crewai_version
|
||||||
@@ -13,7 +14,7 @@ console = Console()
|
|||||||
|
|
||||||
|
|
||||||
class EnterpriseConfigureCommand(BaseCommand):
|
class EnterpriseConfigureCommand(BaseCommand):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.settings_command = SettingsCommand()
|
self.settings_command = SettingsCommand()
|
||||||
|
|
||||||
@@ -54,25 +55,12 @@ class EnterpriseConfigureCommand(BaseCommand):
|
|||||||
except JSONDecodeError as e:
|
except JSONDecodeError as e:
|
||||||
raise ValueError(f"Invalid JSON response from {oauth_endpoint}") from e
|
raise ValueError(f"Invalid JSON response from {oauth_endpoint}") from e
|
||||||
|
|
||||||
required_fields = [
|
self._validate_oauth_config(oauth_config)
|
||||||
"audience",
|
|
||||||
"domain",
|
|
||||||
"device_authorization_client_id",
|
|
||||||
"provider",
|
|
||||||
]
|
|
||||||
missing_fields = [
|
|
||||||
field for field in required_fields if field not in oauth_config
|
|
||||||
]
|
|
||||||
|
|
||||||
if missing_fields:
|
|
||||||
raise ValueError(
|
|
||||||
f"Missing required fields in OAuth2 configuration: {', '.join(missing_fields)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
console.print(
|
console.print(
|
||||||
"✅ Successfully retrieved OAuth2 configuration", style="green"
|
"✅ Successfully retrieved OAuth2 configuration", style="green"
|
||||||
)
|
)
|
||||||
return oauth_config
|
return cast(dict[str, Any], oauth_config)
|
||||||
|
|
||||||
except RequestException as e:
|
except RequestException as e:
|
||||||
raise ValueError(f"Failed to connect to enterprise URL: {e!s}") from e
|
raise ValueError(f"Failed to connect to enterprise URL: {e!s}") from e
|
||||||
@@ -89,6 +77,7 @@ class EnterpriseConfigureCommand(BaseCommand):
|
|||||||
"oauth2_audience": oauth_config["audience"],
|
"oauth2_audience": oauth_config["audience"],
|
||||||
"oauth2_client_id": oauth_config["device_authorization_client_id"],
|
"oauth2_client_id": oauth_config["device_authorization_client_id"],
|
||||||
"oauth2_domain": oauth_config["domain"],
|
"oauth2_domain": oauth_config["domain"],
|
||||||
|
"oauth2_extra": oauth_config["extra"],
|
||||||
}
|
}
|
||||||
|
|
||||||
console.print("🔄 Updating local OAuth2 configuration...")
|
console.print("🔄 Updating local OAuth2 configuration...")
|
||||||
@@ -99,3 +88,38 @@ class EnterpriseConfigureCommand(BaseCommand):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Failed to update OAuth2 settings: {e!s}") from e
|
raise ValueError(f"Failed to update OAuth2 settings: {e!s}") from e
|
||||||
|
|
||||||
|
def _validate_oauth_config(self, oauth_config: dict[str, Any]) -> None:
|
||||||
|
required_fields = [
|
||||||
|
"audience",
|
||||||
|
"domain",
|
||||||
|
"device_authorization_client_id",
|
||||||
|
"provider",
|
||||||
|
"extra",
|
||||||
|
]
|
||||||
|
|
||||||
|
missing_basic_fields = [
|
||||||
|
field for field in required_fields if field not in oauth_config
|
||||||
|
]
|
||||||
|
missing_provider_specific_fields = [
|
||||||
|
field
|
||||||
|
for field in self._get_provider_specific_fields(oauth_config["provider"])
|
||||||
|
if field not in oauth_config.get("extra", {})
|
||||||
|
]
|
||||||
|
|
||||||
|
if missing_basic_fields:
|
||||||
|
raise ValueError(
|
||||||
|
f"Missing required fields in OAuth2 configuration: [{', '.join(missing_basic_fields)}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
if missing_provider_specific_fields:
|
||||||
|
raise ValueError(
|
||||||
|
f"Missing authentication provider required fields in OAuth2 configuration: [{', '.join(missing_provider_specific_fields)}] (Configured provider: '{oauth_config['provider']}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_provider_specific_fields(self, provider_name: str) -> list[str]:
|
||||||
|
provider = ProviderFactory.from_settings(
|
||||||
|
Oauth2Settings(provider=provider_name, client_id="dummy", domain="dummy")
|
||||||
|
)
|
||||||
|
|
||||||
|
return provider.get_required_fields()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import subprocess
|
|||||||
|
|
||||||
|
|
||||||
class Repository:
|
class Repository:
|
||||||
def __init__(self, path="."):
|
def __init__(self, path: str = ".") -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
|
|
||||||
if not self.is_git_installed():
|
if not self.is_git_installed():
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from typing import Any
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -36,19 +37,21 @@ class PlusAPI:
|
|||||||
str(settings.enterprise_base_url) or DEFAULT_CREWAI_ENTERPRISE_URL
|
str(settings.enterprise_base_url) or DEFAULT_CREWAI_ENTERPRISE_URL
|
||||||
)
|
)
|
||||||
|
|
||||||
def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
|
def _make_request(
|
||||||
|
self, method: str, endpoint: str, **kwargs: Any
|
||||||
|
) -> requests.Response:
|
||||||
url = urljoin(self.base_url, endpoint)
|
url = urljoin(self.base_url, endpoint)
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
session.trust_env = False
|
session.trust_env = False
|
||||||
return session.request(method, url, headers=self.headers, **kwargs)
|
return session.request(method, url, headers=self.headers, **kwargs)
|
||||||
|
|
||||||
def login_to_tool_repository(self):
|
def login_to_tool_repository(self) -> requests.Response:
|
||||||
return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login")
|
return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login")
|
||||||
|
|
||||||
def get_tool(self, handle: str):
|
def get_tool(self, handle: str) -> requests.Response:
|
||||||
return self._make_request("GET", f"{self.TOOLS_RESOURCE}/{handle}")
|
return self._make_request("GET", f"{self.TOOLS_RESOURCE}/{handle}")
|
||||||
|
|
||||||
def get_agent(self, handle: str):
|
def get_agent(self, handle: str) -> requests.Response:
|
||||||
return self._make_request("GET", f"{self.AGENTS_RESOURCE}/{handle}")
|
return self._make_request("GET", f"{self.AGENTS_RESOURCE}/{handle}")
|
||||||
|
|
||||||
def publish_tool(
|
def publish_tool(
|
||||||
@@ -58,8 +61,8 @@ class PlusAPI:
|
|||||||
version: str,
|
version: str,
|
||||||
description: str | None,
|
description: str | None,
|
||||||
encoded_file: str,
|
encoded_file: str,
|
||||||
available_exports: list[str] | None = None,
|
available_exports: list[dict[str, Any]] | None = None,
|
||||||
):
|
) -> requests.Response:
|
||||||
params = {
|
params = {
|
||||||
"handle": handle,
|
"handle": handle,
|
||||||
"public": is_public,
|
"public": is_public,
|
||||||
@@ -111,13 +114,13 @@ class PlusAPI:
|
|||||||
def list_crews(self) -> requests.Response:
|
def list_crews(self) -> requests.Response:
|
||||||
return self._make_request("GET", self.CREWS_RESOURCE)
|
return self._make_request("GET", self.CREWS_RESOURCE)
|
||||||
|
|
||||||
def create_crew(self, payload) -> requests.Response:
|
def create_crew(self, payload: dict[str, Any]) -> requests.Response:
|
||||||
return self._make_request("POST", self.CREWS_RESOURCE, json=payload)
|
return self._make_request("POST", self.CREWS_RESOURCE, json=payload)
|
||||||
|
|
||||||
def get_organizations(self) -> requests.Response:
|
def get_organizations(self) -> requests.Response:
|
||||||
return self._make_request("GET", self.ORGANIZATIONS_RESOURCE)
|
return self._make_request("GET", self.ORGANIZATIONS_RESOURCE)
|
||||||
|
|
||||||
def initialize_trace_batch(self, payload) -> requests.Response:
|
def initialize_trace_batch(self, payload: dict[str, Any]) -> requests.Response:
|
||||||
return self._make_request(
|
return self._make_request(
|
||||||
"POST",
|
"POST",
|
||||||
f"{self.TRACING_RESOURCE}/batches",
|
f"{self.TRACING_RESOURCE}/batches",
|
||||||
@@ -125,14 +128,18 @@ class PlusAPI:
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
|
|
||||||
def initialize_ephemeral_trace_batch(self, payload) -> requests.Response:
|
def initialize_ephemeral_trace_batch(
|
||||||
|
self, payload: dict[str, Any]
|
||||||
|
) -> requests.Response:
|
||||||
return self._make_request(
|
return self._make_request(
|
||||||
"POST",
|
"POST",
|
||||||
f"{self.EPHEMERAL_TRACING_RESOURCE}/batches",
|
f"{self.EPHEMERAL_TRACING_RESOURCE}/batches",
|
||||||
json=payload,
|
json=payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_trace_events(self, trace_batch_id: str, payload) -> requests.Response:
|
def send_trace_events(
|
||||||
|
self, trace_batch_id: str, payload: dict[str, Any]
|
||||||
|
) -> requests.Response:
|
||||||
return self._make_request(
|
return self._make_request(
|
||||||
"POST",
|
"POST",
|
||||||
f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}/events",
|
f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}/events",
|
||||||
@@ -141,7 +148,7 @@ class PlusAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def send_ephemeral_trace_events(
|
def send_ephemeral_trace_events(
|
||||||
self, trace_batch_id: str, payload
|
self, trace_batch_id: str, payload: dict[str, Any]
|
||||||
) -> requests.Response:
|
) -> requests.Response:
|
||||||
return self._make_request(
|
return self._make_request(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -150,7 +157,9 @@ class PlusAPI:
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
|
|
||||||
def finalize_trace_batch(self, trace_batch_id: str, payload) -> requests.Response:
|
def finalize_trace_batch(
|
||||||
|
self, trace_batch_id: str, payload: dict[str, Any]
|
||||||
|
) -> requests.Response:
|
||||||
return self._make_request(
|
return self._make_request(
|
||||||
"PATCH",
|
"PATCH",
|
||||||
f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}/finalize",
|
f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}/finalize",
|
||||||
@@ -159,7 +168,7 @@ class PlusAPI:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def finalize_ephemeral_trace_batch(
|
def finalize_ephemeral_trace_batch(
|
||||||
self, trace_batch_id: str, payload
|
self, trace_batch_id: str, payload: dict[str, Any]
|
||||||
) -> requests.Response:
|
) -> requests.Response:
|
||||||
return self._make_request(
|
return self._make_request(
|
||||||
"PATCH",
|
"PATCH",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class SettingsCommand(BaseCommand):
|
|||||||
current_value = getattr(self.settings, field_name)
|
current_value = getattr(self.settings, field_name)
|
||||||
description = field_info.description or "No description available"
|
description = field_info.description or "No description available"
|
||||||
display_value = (
|
display_value = (
|
||||||
str(current_value) if current_value is not None else "Not set"
|
str(current_value) if current_value not in [None, {}] else "Not set"
|
||||||
)
|
)
|
||||||
|
|
||||||
table.add_row(field_name, display_value, description)
|
table.add_row(field_name, display_value, description)
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
A class to handle tool repository related operations for CrewAI projects.
|
A class to handle tool repository related operations for CrewAI projects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
BaseCommand.__init__(self)
|
BaseCommand.__init__(self)
|
||||||
PlusAPIMixin.__init__(self, telemetry=self._telemetry)
|
PlusAPIMixin.__init__(self, telemetry=self._telemetry)
|
||||||
|
|
||||||
def create(self, handle: str):
|
def create(self, handle: str) -> None:
|
||||||
self._ensure_not_in_project()
|
self._ensure_not_in_project()
|
||||||
|
|
||||||
folder_name = handle.replace(" ", "_").replace("-", "_").lower()
|
folder_name = handle.replace(" ", "_").replace("-", "_").lower()
|
||||||
@@ -64,7 +64,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
finally:
|
finally:
|
||||||
os.chdir(old_directory)
|
os.chdir(old_directory)
|
||||||
|
|
||||||
def publish(self, is_public: bool, force: bool = False):
|
def publish(self, is_public: bool, force: bool = False) -> None:
|
||||||
if not git.Repository().is_synced() and not force:
|
if not git.Repository().is_synced() and not force:
|
||||||
console.print(
|
console.print(
|
||||||
"[bold red]Failed to publish tool.[/bold red]\n"
|
"[bold red]Failed to publish tool.[/bold red]\n"
|
||||||
@@ -137,7 +137,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
style="bold green",
|
style="bold green",
|
||||||
)
|
)
|
||||||
|
|
||||||
def install(self, handle: str):
|
def install(self, handle: str) -> None:
|
||||||
self._print_current_organization()
|
self._print_current_organization()
|
||||||
get_response = self.plus_api_client.get_tool(handle)
|
get_response = self.plus_api_client.get_tool(handle)
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
settings.org_name = login_response_json["current_organization"]["name"]
|
settings.org_name = login_response_json["current_organization"]["name"]
|
||||||
settings.dump()
|
settings.dump()
|
||||||
|
|
||||||
def _add_package(self, tool_details: dict[str, Any]):
|
def _add_package(self, tool_details: dict[str, Any]) -> None:
|
||||||
is_from_pypi = tool_details.get("source", None) == "pypi"
|
is_from_pypi = tool_details.get("source", None) == "pypi"
|
||||||
tool_handle = tool_details["handle"]
|
tool_handle = tool_details["handle"]
|
||||||
repository_handle = tool_details["repository"]["handle"]
|
repository_handle = tool_details["repository"]["handle"]
|
||||||
@@ -209,7 +209,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
|||||||
click.echo(add_package_result.stderr, err=True)
|
click.echo(add_package_result.stderr, err=True)
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
|
|
||||||
def _ensure_not_in_project(self):
|
def _ensure_not_in_project(self) -> None:
|
||||||
if os.path.isfile("./pyproject.toml"):
|
if os.path.isfile("./pyproject.toml"):
|
||||||
console.print(
|
console.print(
|
||||||
"[bold red]Oops! It looks like you're inside a project.[/bold red]"
|
"[bold red]Oops! It looks like you're inside a project.[/bold red]"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, get_type_hints
|
from typing import Any, cast, get_type_hints
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
@@ -23,7 +23,9 @@ if sys.version_info >= (3, 11):
|
|||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
def copy_template(src, dst, name, class_name, folder_name):
|
def copy_template(
|
||||||
|
src: Path, dst: Path, name: str, class_name: str, folder_name: str
|
||||||
|
) -> None:
|
||||||
"""Copy a file from src to dst."""
|
"""Copy a file from src to dst."""
|
||||||
with open(src, "r") as file:
|
with open(src, "r") as file:
|
||||||
content = file.read()
|
content = file.read()
|
||||||
@@ -40,13 +42,13 @@ def copy_template(src, dst, name, class_name, folder_name):
|
|||||||
click.secho(f" - Created {dst}", fg="green")
|
click.secho(f" - Created {dst}", fg="green")
|
||||||
|
|
||||||
|
|
||||||
def read_toml(file_path: str = "pyproject.toml"):
|
def read_toml(file_path: str = "pyproject.toml") -> dict[str, Any]:
|
||||||
"""Read the content of a TOML file and return it as a dictionary."""
|
"""Read the content of a TOML file and return it as a dictionary."""
|
||||||
with open(file_path, "rb") as f:
|
with open(file_path, "rb") as f:
|
||||||
return tomli.load(f)
|
return tomli.load(f)
|
||||||
|
|
||||||
|
|
||||||
def parse_toml(content):
|
def parse_toml(content: str) -> dict[str, Any]:
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
return tomllib.loads(content)
|
return tomllib.loads(content)
|
||||||
return tomli.loads(content)
|
return tomli.loads(content)
|
||||||
@@ -103,7 +105,7 @@ def _get_project_attribute(
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle TOML decode errors for Python 3.11+
|
# Handle TOML decode errors for Python 3.11+
|
||||||
if sys.version_info >= (3, 11) and isinstance(e, tomllib.TOMLDecodeError): # type: ignore
|
if sys.version_info >= (3, 11) and isinstance(e, tomllib.TOMLDecodeError):
|
||||||
console.print(
|
console.print(
|
||||||
f"Error: {pyproject_path} is not a valid TOML file.", style="bold red"
|
f"Error: {pyproject_path} is not a valid TOML file.", style="bold red"
|
||||||
)
|
)
|
||||||
@@ -126,7 +128,7 @@ def _get_nested_value(data: dict[str, Any], keys: list[str]) -> Any:
|
|||||||
return reduce(dict.__getitem__, keys, data)
|
return reduce(dict.__getitem__, keys, data)
|
||||||
|
|
||||||
|
|
||||||
def fetch_and_json_env_file(env_file_path: str = ".env") -> dict:
|
def fetch_and_json_env_file(env_file_path: str = ".env") -> dict[str, Any]:
|
||||||
"""Fetch the environment variables from a .env file and return them as a dictionary."""
|
"""Fetch the environment variables from a .env file and return them as a dictionary."""
|
||||||
try:
|
try:
|
||||||
# Read the .env file
|
# Read the .env file
|
||||||
@@ -150,7 +152,7 @@ def fetch_and_json_env_file(env_file_path: str = ".env") -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def tree_copy(source, destination):
|
def tree_copy(source: Path, destination: Path) -> None:
|
||||||
"""Copies the entire directory structure from the source to the destination."""
|
"""Copies the entire directory structure from the source to the destination."""
|
||||||
for item in os.listdir(source):
|
for item in os.listdir(source):
|
||||||
source_item = os.path.join(source, item)
|
source_item = os.path.join(source, item)
|
||||||
@@ -161,7 +163,7 @@ def tree_copy(source, destination):
|
|||||||
shutil.copy2(source_item, destination_item)
|
shutil.copy2(source_item, destination_item)
|
||||||
|
|
||||||
|
|
||||||
def tree_find_and_replace(directory, find, replace):
|
def tree_find_and_replace(directory: Path, find: str, replace: str) -> None:
|
||||||
"""Recursively searches through a directory, replacing a target string in
|
"""Recursively searches through a directory, replacing a target string in
|
||||||
both file contents and filenames with a specified replacement string.
|
both file contents and filenames with a specified replacement string.
|
||||||
"""
|
"""
|
||||||
@@ -187,7 +189,7 @@ def tree_find_and_replace(directory, find, replace):
|
|||||||
os.rename(old_dirpath, new_dirpath)
|
os.rename(old_dirpath, new_dirpath)
|
||||||
|
|
||||||
|
|
||||||
def load_env_vars(folder_path):
|
def load_env_vars(folder_path: Path) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Loads environment variables from a .env file in the specified folder path.
|
Loads environment variables from a .env file in the specified folder path.
|
||||||
|
|
||||||
@@ -208,7 +210,9 @@ def load_env_vars(folder_path):
|
|||||||
return env_vars
|
return env_vars
|
||||||
|
|
||||||
|
|
||||||
def update_env_vars(env_vars, provider, model):
|
def update_env_vars(
|
||||||
|
env_vars: dict[str, Any], provider: str, model: str
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
"""
|
"""
|
||||||
Updates environment variables with the API key for the selected provider and model.
|
Updates environment variables with the API key for the selected provider and model.
|
||||||
|
|
||||||
@@ -220,15 +224,20 @@ def update_env_vars(env_vars, provider, model):
|
|||||||
Returns:
|
Returns:
|
||||||
- None
|
- None
|
||||||
"""
|
"""
|
||||||
api_key_var = ENV_VARS.get(
|
provider_config = cast(
|
||||||
provider,
|
list[str],
|
||||||
[
|
ENV_VARS.get(
|
||||||
click.prompt(
|
provider,
|
||||||
f"Enter the environment variable name for your {provider.capitalize()} API key",
|
[
|
||||||
type=str,
|
click.prompt(
|
||||||
)
|
f"Enter the environment variable name for your {provider.capitalize()} API key",
|
||||||
],
|
type=str,
|
||||||
)[0]
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
api_key_var = provider_config[0]
|
||||||
|
|
||||||
if api_key_var not in env_vars:
|
if api_key_var not in env_vars:
|
||||||
try:
|
try:
|
||||||
@@ -246,7 +255,7 @@ def update_env_vars(env_vars, provider, model):
|
|||||||
return env_vars
|
return env_vars
|
||||||
|
|
||||||
|
|
||||||
def write_env_file(folder_path, env_vars):
|
def write_env_file(folder_path: Path, env_vars: dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Writes environment variables to a .env file in the specified folder.
|
Writes environment variables to a .env file in the specified folder.
|
||||||
|
|
||||||
@@ -342,18 +351,18 @@ def get_crews(crew_path: str = "crew.py", require: bool = False) -> list[Crew]:
|
|||||||
return crew_instances
|
return crew_instances
|
||||||
|
|
||||||
|
|
||||||
def get_crew_instance(module_attr) -> Crew | None:
|
def get_crew_instance(module_attr: Any) -> Crew | None:
|
||||||
if (
|
if (
|
||||||
callable(module_attr)
|
callable(module_attr)
|
||||||
and hasattr(module_attr, "is_crew_class")
|
and hasattr(module_attr, "is_crew_class")
|
||||||
and module_attr.is_crew_class
|
and module_attr.is_crew_class
|
||||||
):
|
):
|
||||||
return module_attr().crew()
|
return cast(Crew, module_attr().crew())
|
||||||
try:
|
try:
|
||||||
if (ismethod(module_attr) or isfunction(module_attr)) and get_type_hints(
|
if (ismethod(module_attr) or isfunction(module_attr)) and get_type_hints(
|
||||||
module_attr
|
module_attr
|
||||||
).get("return") is Crew:
|
).get("return") is Crew:
|
||||||
return module_attr()
|
return cast(Crew, module_attr())
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -362,7 +371,7 @@ def get_crew_instance(module_attr) -> Crew | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def fetch_crews(module_attr) -> list[Crew]:
|
def fetch_crews(module_attr: Any) -> list[Crew]:
|
||||||
crew_instances: list[Crew] = []
|
crew_instances: list[Crew] = []
|
||||||
|
|
||||||
if crew_instance := get_crew_instance(module_attr):
|
if crew_instance := get_crew_instance(module_attr):
|
||||||
@@ -377,7 +386,7 @@ def fetch_crews(module_attr) -> list[Crew]:
|
|||||||
return crew_instances
|
return crew_instances
|
||||||
|
|
||||||
|
|
||||||
def is_valid_tool(obj):
|
def is_valid_tool(obj: Any) -> bool:
|
||||||
from crewai.tools.base_tool import Tool
|
from crewai.tools.base_tool import Tool
|
||||||
|
|
||||||
if isclass(obj):
|
if isclass(obj):
|
||||||
@@ -389,7 +398,7 @@ def is_valid_tool(obj):
|
|||||||
return isinstance(obj, Tool)
|
return isinstance(obj, Tool)
|
||||||
|
|
||||||
|
|
||||||
def extract_available_exports(dir_path: str = "src"):
|
def extract_available_exports(dir_path: str = "src") -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Extract available tool classes from the project's __init__.py files.
|
Extract available tool classes from the project's __init__.py files.
|
||||||
Only includes classes that inherit from BaseTool or functions decorated with @tool.
|
Only includes classes that inherit from BaseTool or functions decorated with @tool.
|
||||||
@@ -419,7 +428,9 @@ def extract_available_exports(dir_path: str = "src"):
|
|||||||
raise SystemExit(1) from e
|
raise SystemExit(1) from e
|
||||||
|
|
||||||
|
|
||||||
def build_env_with_tool_repository_credentials(repository_handle: str):
|
def build_env_with_tool_repository_credentials(
|
||||||
|
repository_handle: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
repository_handle = repository_handle.upper().replace("-", "_")
|
repository_handle = repository_handle.upper().replace("-", "_")
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
@@ -472,7 +483,7 @@ def _load_tools_from_init(init_file: Path) -> list[dict[str, Any]]:
|
|||||||
sys.modules.pop("temp_module", None)
|
sys.modules.pop("temp_module", None)
|
||||||
|
|
||||||
|
|
||||||
def _print_no_tools_warning():
|
def _print_no_tools_warning() -> None:
|
||||||
"""
|
"""
|
||||||
Display warning and usage instructions if no tools were found.
|
Display warning and usage instructions if no tools were found.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from crewai.utilities.types import LLMMessage
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from crewai.agent import Agent
|
from crewai.agent import Agent
|
||||||
|
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
||||||
from crewai.lite_agent import LiteAgent
|
from crewai.lite_agent import LiteAgent
|
||||||
from crewai.llm import LLM
|
from crewai.llm import LLM
|
||||||
from crewai.task import Task
|
from crewai.task import Task
|
||||||
@@ -236,6 +237,7 @@ def get_llm_response(
|
|||||||
from_task: Task | None = None,
|
from_task: Task | None = None,
|
||||||
from_agent: Agent | LiteAgent | None = None,
|
from_agent: Agent | LiteAgent | None = None,
|
||||||
response_model: type[BaseModel] | None = None,
|
response_model: type[BaseModel] | None = None,
|
||||||
|
executor_context: CrewAgentExecutor | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Call the LLM and return the response, handling any invalid responses.
|
"""Call the LLM and return the response, handling any invalid responses.
|
||||||
|
|
||||||
@@ -247,6 +249,7 @@ def get_llm_response(
|
|||||||
from_task: Optional task context for the LLM call
|
from_task: Optional task context for the LLM call
|
||||||
from_agent: Optional agent context for the LLM call
|
from_agent: Optional agent context for the LLM call
|
||||||
response_model: Optional Pydantic model for structured outputs
|
response_model: Optional Pydantic model for structured outputs
|
||||||
|
executor_context: Optional executor context for hook invocation
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The response from the LLM as a string
|
The response from the LLM as a string
|
||||||
@@ -255,6 +258,11 @@ def get_llm_response(
|
|||||||
Exception: If an error occurs.
|
Exception: If an error occurs.
|
||||||
ValueError: If the response is None or empty.
|
ValueError: If the response is None or empty.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if executor_context is not None:
|
||||||
|
_setup_before_llm_call_hooks(executor_context, printer)
|
||||||
|
messages = executor_context.messages
|
||||||
|
|
||||||
try:
|
try:
|
||||||
answer = llm.call(
|
answer = llm.call(
|
||||||
messages,
|
messages,
|
||||||
@@ -272,7 +280,7 @@ def get_llm_response(
|
|||||||
)
|
)
|
||||||
raise ValueError("Invalid response from LLM call - None or empty.")
|
raise ValueError("Invalid response from LLM call - None or empty.")
|
||||||
|
|
||||||
return answer
|
return _setup_after_llm_call_hooks(executor_context, answer, printer)
|
||||||
|
|
||||||
|
|
||||||
def process_llm_response(
|
def process_llm_response(
|
||||||
@@ -661,3 +669,92 @@ def load_agent_from_repository(from_repository: str) -> dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
attributes[key] = value
|
attributes[key] = value
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_before_llm_call_hooks(
|
||||||
|
executor_context: CrewAgentExecutor | None, printer: Printer
|
||||||
|
) -> None:
|
||||||
|
"""Setup and invoke before_llm_call hooks for the executor context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
executor_context: The executor context to setup the hooks for.
|
||||||
|
printer: Printer instance for error logging.
|
||||||
|
"""
|
||||||
|
if executor_context and executor_context.before_llm_call_hooks:
|
||||||
|
from crewai.utilities.llm_call_hooks import LLMCallHookContext
|
||||||
|
|
||||||
|
original_messages = executor_context.messages
|
||||||
|
|
||||||
|
hook_context = LLMCallHookContext(executor_context)
|
||||||
|
try:
|
||||||
|
for hook in executor_context.before_llm_call_hooks:
|
||||||
|
hook(hook_context)
|
||||||
|
except Exception as e:
|
||||||
|
printer.print(
|
||||||
|
content=f"Error in before_llm_call hook: {e}",
|
||||||
|
color="yellow",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(executor_context.messages, list):
|
||||||
|
printer.print(
|
||||||
|
content=(
|
||||||
|
"Warning: before_llm_call hook replaced messages with non-list. "
|
||||||
|
"Restoring original messages list. Hooks should modify messages in-place, "
|
||||||
|
"not replace the list (e.g., use context.messages.append() not context.messages = [])."
|
||||||
|
),
|
||||||
|
color="yellow",
|
||||||
|
)
|
||||||
|
if isinstance(original_messages, list):
|
||||||
|
executor_context.messages = original_messages
|
||||||
|
else:
|
||||||
|
executor_context.messages = []
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_after_llm_call_hooks(
|
||||||
|
executor_context: CrewAgentExecutor | None,
|
||||||
|
answer: str,
|
||||||
|
printer: Printer,
|
||||||
|
) -> str:
|
||||||
|
"""Setup and invoke after_llm_call hooks for the executor context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
executor_context: The executor context to setup the hooks for.
|
||||||
|
answer: The LLM response string.
|
||||||
|
printer: Printer instance for error logging.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The potentially modified response string.
|
||||||
|
"""
|
||||||
|
if executor_context and executor_context.after_llm_call_hooks:
|
||||||
|
from crewai.utilities.llm_call_hooks import LLMCallHookContext
|
||||||
|
|
||||||
|
original_messages = executor_context.messages
|
||||||
|
|
||||||
|
hook_context = LLMCallHookContext(executor_context, response=answer)
|
||||||
|
try:
|
||||||
|
for hook in executor_context.after_llm_call_hooks:
|
||||||
|
modified_response = hook(hook_context)
|
||||||
|
if modified_response is not None and isinstance(modified_response, str):
|
||||||
|
answer = modified_response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
printer.print(
|
||||||
|
content=f"Error in after_llm_call hook: {e}",
|
||||||
|
color="yellow",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(executor_context.messages, list):
|
||||||
|
printer.print(
|
||||||
|
content=(
|
||||||
|
"Warning: after_llm_call hook replaced messages with non-list. "
|
||||||
|
"Restoring original messages list. Hooks should modify messages in-place, "
|
||||||
|
"not replace the list (e.g., use context.messages.append() not context.messages = [])."
|
||||||
|
),
|
||||||
|
color="yellow",
|
||||||
|
)
|
||||||
|
if isinstance(original_messages, list):
|
||||||
|
executor_context.messages = original_messages
|
||||||
|
else:
|
||||||
|
executor_context.messages = []
|
||||||
|
|
||||||
|
return answer
|
||||||
|
|||||||
115
lib/crewai/src/crewai/utilities/llm_call_hooks.py
Normal file
115
lib/crewai/src/crewai/utilities/llm_call_hooks.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
||||||
|
|
||||||
|
|
||||||
|
class LLMCallHookContext:
|
||||||
|
"""Context object passed to LLM call hooks with full executor access.
|
||||||
|
|
||||||
|
Provides hooks with complete access to the executor state, allowing
|
||||||
|
modification of messages, responses, and executor attributes.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
executor: Full reference to the CrewAgentExecutor instance
|
||||||
|
messages: Direct reference to executor.messages (mutable list).
|
||||||
|
Can be modified in both before_llm_call and after_llm_call hooks.
|
||||||
|
Modifications in after_llm_call hooks persist to the next iteration,
|
||||||
|
allowing hooks to modify conversation history for subsequent LLM calls.
|
||||||
|
IMPORTANT: Modify messages in-place (e.g., append, extend, remove items).
|
||||||
|
Do NOT replace the list (e.g., context.messages = []), as this will break
|
||||||
|
the executor. Use context.messages.append() or context.messages.extend()
|
||||||
|
instead of assignment.
|
||||||
|
agent: Reference to the agent executing the task
|
||||||
|
task: Reference to the task being executed
|
||||||
|
crew: Reference to the crew instance
|
||||||
|
llm: Reference to the LLM instance
|
||||||
|
iterations: Current iteration count
|
||||||
|
response: LLM response string (only set for after_llm_call hooks).
|
||||||
|
Can be modified by returning a new string from after_llm_call hook.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
executor: CrewAgentExecutor,
|
||||||
|
response: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize hook context with executor reference.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
executor: The CrewAgentExecutor instance
|
||||||
|
response: Optional response string (for after_llm_call hooks)
|
||||||
|
"""
|
||||||
|
self.executor = executor
|
||||||
|
self.messages = executor.messages
|
||||||
|
self.agent = executor.agent
|
||||||
|
self.task = executor.task
|
||||||
|
self.crew = executor.crew
|
||||||
|
self.llm = executor.llm
|
||||||
|
self.iterations = executor.iterations
|
||||||
|
self.response = response
|
||||||
|
|
||||||
|
|
||||||
|
# Global hook registries (optional convenience feature)
|
||||||
|
_before_llm_call_hooks: list[Callable[[LLMCallHookContext], None]] = []
|
||||||
|
_after_llm_call_hooks: list[Callable[[LLMCallHookContext], str | None]] = []
|
||||||
|
|
||||||
|
|
||||||
|
def register_before_llm_call_hook(
|
||||||
|
hook: Callable[[LLMCallHookContext], None],
|
||||||
|
) -> None:
|
||||||
|
"""Register a global before_llm_call hook.
|
||||||
|
|
||||||
|
Global hooks are added to all executors automatically.
|
||||||
|
This is a convenience function for registering hooks that should
|
||||||
|
apply to all LLM calls across all executors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hook: Function that receives LLMCallHookContext and can modify
|
||||||
|
context.messages directly. Should return None.
|
||||||
|
IMPORTANT: Modify messages in-place (append, extend, remove items).
|
||||||
|
Do NOT replace the list (context.messages = []), as this will break execution.
|
||||||
|
"""
|
||||||
|
_before_llm_call_hooks.append(hook)
|
||||||
|
|
||||||
|
|
||||||
|
def register_after_llm_call_hook(
|
||||||
|
hook: Callable[[LLMCallHookContext], str | None],
|
||||||
|
) -> None:
|
||||||
|
"""Register a global after_llm_call hook.
|
||||||
|
|
||||||
|
Global hooks are added to all executors automatically.
|
||||||
|
This is a convenience function for registering hooks that should
|
||||||
|
apply to all LLM calls across all executors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hook: Function that receives LLMCallHookContext and can modify:
|
||||||
|
- The response: Return modified response string or None to keep original
|
||||||
|
- The messages: Modify context.messages directly (mutable reference)
|
||||||
|
Both modifications are supported and can be used together.
|
||||||
|
IMPORTANT: Modify messages in-place (append, extend, remove items).
|
||||||
|
Do NOT replace the list (context.messages = []), as this will break execution.
|
||||||
|
"""
|
||||||
|
_after_llm_call_hooks.append(hook)
|
||||||
|
|
||||||
|
|
||||||
|
def get_before_llm_call_hooks() -> list[Callable[[LLMCallHookContext], None]]:
|
||||||
|
"""Get all registered global before_llm_call hooks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of registered before hooks
|
||||||
|
"""
|
||||||
|
return _before_llm_call_hooks.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def get_after_llm_call_hooks() -> list[Callable[[LLMCallHookContext], str | None]]:
|
||||||
|
"""Get all registered global after_llm_call hooks.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of registered after hooks
|
||||||
|
"""
|
||||||
|
return _after_llm_call_hooks.copy()
|
||||||
@@ -2714,3 +2714,293 @@ def test_agent_without_apps_no_platform_tools():
|
|||||||
|
|
||||||
tools = crew._prepare_tools(agent, task, [])
|
tools = crew._prepare_tools(agent, task, [])
|
||||||
assert tools == []
|
assert tools == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_before_llm_call_hook_modifies_messages():
|
||||||
|
"""Test that before_llm_call hooks can modify messages."""
|
||||||
|
from crewai.utilities.llm_call_hooks import LLMCallHookContext, register_before_llm_call_hook
|
||||||
|
|
||||||
|
hook_called = False
|
||||||
|
original_message_count = 0
|
||||||
|
|
||||||
|
def before_hook(context: LLMCallHookContext) -> None:
|
||||||
|
nonlocal hook_called, original_message_count
|
||||||
|
hook_called = True
|
||||||
|
original_message_count = len(context.messages)
|
||||||
|
context.messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": "Additional context: This is a test modification."
|
||||||
|
})
|
||||||
|
|
||||||
|
register_before_llm_call_hook(before_hook)
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
allow_delegation=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Say hello",
|
||||||
|
expected_output="A greeting",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = agent.execute_task(task)
|
||||||
|
|
||||||
|
assert hook_called, "before_llm_call hook should have been called"
|
||||||
|
assert len(agent.agent_executor.messages) > original_message_count
|
||||||
|
assert result is not None
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_after_llm_call_hook_modifies_messages_for_next_iteration():
|
||||||
|
"""Test that after_llm_call hooks can modify messages for the next iteration."""
|
||||||
|
from crewai.utilities.llm_call_hooks import LLMCallHookContext, register_after_llm_call_hook
|
||||||
|
|
||||||
|
hook_call_count = 0
|
||||||
|
hook_iterations = []
|
||||||
|
messages_added_in_iteration_0 = False
|
||||||
|
test_message_content = "HOOK_ADDED_MESSAGE_FOR_NEXT_ITERATION"
|
||||||
|
|
||||||
|
def after_hook(context: LLMCallHookContext) -> str | None:
|
||||||
|
nonlocal hook_call_count, hook_iterations, messages_added_in_iteration_0
|
||||||
|
hook_call_count += 1
|
||||||
|
current_iteration = context.iterations
|
||||||
|
hook_iterations.append(current_iteration)
|
||||||
|
|
||||||
|
if current_iteration == 0:
|
||||||
|
messages_before = len(context.messages)
|
||||||
|
context.messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": test_message_content
|
||||||
|
})
|
||||||
|
messages_added_in_iteration_0 = True
|
||||||
|
assert len(context.messages) == messages_before + 1
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
register_after_llm_call_hook(after_hook)
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
allow_delegation=False,
|
||||||
|
max_iter=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Count to 3, taking your time",
|
||||||
|
expected_output="A count",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = agent.execute_task(task)
|
||||||
|
|
||||||
|
assert hook_call_count > 0, "after_llm_call hook should have been called"
|
||||||
|
assert messages_added_in_iteration_0, "Message should have been added in iteration 0"
|
||||||
|
|
||||||
|
executor_messages = agent.agent_executor.messages
|
||||||
|
message_contents = [msg.get("content", "") for msg in executor_messages if isinstance(msg, dict)]
|
||||||
|
assert any(test_message_content in content for content in message_contents), (
|
||||||
|
f"Message added by hook in iteration 0 should be present in executor messages. "
|
||||||
|
f"Messages: {message_contents}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(executor_messages) > 2, "Executor should have more than initial messages"
|
||||||
|
assert result is not None
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_after_llm_call_hook_modifies_messages():
|
||||||
|
"""Test that after_llm_call hooks can modify messages for next iteration."""
|
||||||
|
from crewai.utilities.llm_call_hooks import LLMCallHookContext, register_after_llm_call_hook
|
||||||
|
|
||||||
|
hook_called = False
|
||||||
|
messages_before_hook = 0
|
||||||
|
|
||||||
|
def after_hook(context: LLMCallHookContext) -> str | None:
|
||||||
|
nonlocal hook_called, messages_before_hook
|
||||||
|
hook_called = True
|
||||||
|
messages_before_hook = len(context.messages)
|
||||||
|
context.messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": "Remember: This is iteration 2 context."
|
||||||
|
})
|
||||||
|
return None # Don't modify response
|
||||||
|
|
||||||
|
register_after_llm_call_hook(after_hook)
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
allow_delegation=False,
|
||||||
|
max_iter=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Count to 2",
|
||||||
|
expected_output="A count",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = agent.execute_task(task)
|
||||||
|
|
||||||
|
assert hook_called, "after_llm_call hook should have been called"
|
||||||
|
assert len(agent.agent_executor.messages) > messages_before_hook
|
||||||
|
assert result is not None
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_llm_call_hooks_with_crew():
|
||||||
|
"""Test that LLM call hooks work with crew execution."""
|
||||||
|
from crewai.utilities.llm_call_hooks import (
|
||||||
|
LLMCallHookContext,
|
||||||
|
register_after_llm_call_hook,
|
||||||
|
register_before_llm_call_hook,
|
||||||
|
)
|
||||||
|
|
||||||
|
before_hook_called = False
|
||||||
|
after_hook_called = False
|
||||||
|
|
||||||
|
def before_hook(context: LLMCallHookContext) -> None:
|
||||||
|
nonlocal before_hook_called
|
||||||
|
before_hook_called = True
|
||||||
|
assert context.executor is not None
|
||||||
|
assert context.agent is not None
|
||||||
|
assert context.task is not None
|
||||||
|
context.messages.append({
|
||||||
|
"role": "system",
|
||||||
|
"content": "Additional system context from hook."
|
||||||
|
})
|
||||||
|
|
||||||
|
def after_hook(context: LLMCallHookContext) -> str | None:
|
||||||
|
nonlocal after_hook_called
|
||||||
|
after_hook_called = True
|
||||||
|
assert context.response is not None
|
||||||
|
assert len(context.messages) > 0
|
||||||
|
return None
|
||||||
|
|
||||||
|
register_before_llm_call_hook(before_hook)
|
||||||
|
register_after_llm_call_hook(after_hook)
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent = Agent(
|
||||||
|
role="Researcher",
|
||||||
|
goal="Research topics",
|
||||||
|
backstory="You are a researcher",
|
||||||
|
allow_delegation=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Research AI frameworks",
|
||||||
|
expected_output="A research summary",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
crew = Crew(agents=[agent], tasks=[task])
|
||||||
|
result = crew.kickoff()
|
||||||
|
|
||||||
|
assert before_hook_called, "before_llm_call hook should have been called"
|
||||||
|
assert after_hook_called, "after_llm_call hook should have been called"
|
||||||
|
assert result is not None
|
||||||
|
assert result.raw is not None
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_llm_call_hooks_can_modify_executor_attributes():
|
||||||
|
"""Test that hooks can access and modify executor attributes like tools."""
|
||||||
|
from crewai.utilities.llm_call_hooks import LLMCallHookContext, register_before_llm_call_hook
|
||||||
|
from crewai.tools import tool
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def test_tool() -> str:
|
||||||
|
"""A test tool."""
|
||||||
|
return "test result"
|
||||||
|
|
||||||
|
hook_called = False
|
||||||
|
original_tools_count = 0
|
||||||
|
|
||||||
|
def before_hook(context: LLMCallHookContext) -> None:
|
||||||
|
nonlocal hook_called, original_tools_count
|
||||||
|
hook_called = True
|
||||||
|
original_tools_count = len(context.executor.tools)
|
||||||
|
assert context.executor.max_iter > 0
|
||||||
|
assert context.executor.iterations >= 0
|
||||||
|
assert context.executor.tools is not None
|
||||||
|
|
||||||
|
register_before_llm_call_hook(before_hook)
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
tools=[test_tool],
|
||||||
|
allow_delegation=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Use the test tool",
|
||||||
|
expected_output="Tool result",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = agent.execute_task(task)
|
||||||
|
|
||||||
|
assert hook_called, "before_llm_call hook should have been called"
|
||||||
|
assert original_tools_count >= 0
|
||||||
|
assert result is not None
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_llm_call_hooks_error_handling():
|
||||||
|
"""Test that hook errors don't break execution."""
|
||||||
|
from crewai.utilities.llm_call_hooks import LLMCallHookContext, register_before_llm_call_hook
|
||||||
|
|
||||||
|
hook_called = False
|
||||||
|
|
||||||
|
def error_hook(context: LLMCallHookContext) -> None:
|
||||||
|
nonlocal hook_called
|
||||||
|
hook_called = True
|
||||||
|
raise ValueError("Test hook error")
|
||||||
|
|
||||||
|
register_before_llm_call_hook(error_hook)
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test goal",
|
||||||
|
backstory="Test backstory",
|
||||||
|
allow_delegation=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Say hello",
|
||||||
|
expected_output="A greeting",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = agent.execute_task(task)
|
||||||
|
|
||||||
|
assert hook_called, "before_llm_call hook should have been called"
|
||||||
|
assert result is not None
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour
|
||||||
|
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||||
|
respond using the exact following format:\n\nThought: I now can give a great
|
||||||
|
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||||
|
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||||
|
depends on it!"},{"role":"user","content":"\nCurrent Task: Count to 2\n\nThis
|
||||||
|
is the expected criteria for your final answer: A count\nyou MUST return the
|
||||||
|
actual complete content as the final answer, not a summary.\n\nBegin! This is
|
||||||
|
VERY important to you, use the tools available and give your best Final Answer,
|
||||||
|
your job depends on it!\n\nThought:"},{"role":"user","content":"Additional context:
|
||||||
|
This is a test modification."}],"model":"gpt-4.1-mini"}'
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- application/json
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '849'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
host:
|
||||||
|
- api.openai.com
|
||||||
|
user-agent:
|
||||||
|
- OpenAI/Python 1.109.1
|
||||||
|
x-stainless-arch:
|
||||||
|
- arm64
|
||||||
|
x-stainless-async:
|
||||||
|
- 'false'
|
||||||
|
x-stainless-lang:
|
||||||
|
- python
|
||||||
|
x-stainless-os:
|
||||||
|
- MacOS
|
||||||
|
x-stainless-package-version:
|
||||||
|
- 1.109.1
|
||||||
|
x-stainless-read-timeout:
|
||||||
|
- '600'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
|
x-stainless-runtime:
|
||||||
|
- CPython
|
||||||
|
x-stainless-runtime-version:
|
||||||
|
- 3.13.3
|
||||||
|
method: POST
|
||||||
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: !!binary |
|
||||||
|
H4sIAAAAAAAAAwAAAP//jFJNb5wwEL3zK0Y+QwSI7LLcokqVcujHoR9S2wg5ZsBujceyTdIo2v9e
|
||||||
|
GTYLaROpFyTmzXt+b2YeEwCmOtYAE5IHMVqdveH09UHKLx+/2eFzkAdZXL8XJPr9h3dFydLIoNuf
|
||||||
|
KMIT60LQaDUGRWaBhUMeMKoW+11ZH/K8rmZgpA51pA02ZNVFkY3KqKzMy8ssr7KiOtElKYGeNfA9
|
||||||
|
AQB4nL/RqOnwN2sgT58qI3rPB2TNuQmAOdKxwrj3ygduAktXUJAJaGbvnyRNgwwNXIOhexDcwKDu
|
||||||
|
EDgMMQBw4+/R/TBvleEarua/BooUyq2gw37yPKYyk9YbgBtDgcepzFFuTsjxbF7TYB3d+r+orFdG
|
||||||
|
edk65J5MNOoDWTajxwTgZh7S9Cw3s45GG9pAv3B+rtgdFj22LmeD1icwUOB6W9+nL+i1HQautN+M
|
||||||
|
mQkuJHYrdd0JnzpFGyDZpP7XzUvaS3Jlhv+RXwEh0AbsWuuwU+J54rXNYbzd19rOU54NM4/uTgls
|
||||||
|
g0IXN9Fhzye9HBTzDz7g2PbKDOisU8tV9batRFlfFn29K1lyTP4AAAD//wMApumqgWQDAAA=
|
||||||
|
headers:
|
||||||
|
CF-RAY:
|
||||||
|
- 99d044543db94e48-SJC
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Tue, 11 Nov 2025 19:41:25 GMT
|
||||||
|
Server:
|
||||||
|
- cloudflare
|
||||||
|
Set-Cookie:
|
||||||
|
- __cf_bm=KLlCOQ_zxXquDvj96O28ObVFEoAbFE8R7zlmuiuXH1M-1762890085-1.0.1.1-UChItG1GnLDHrErY60dUpkbD3lEkSvfkTQpOmEtzd0fjjm_y1pJQiB.VDXVi2pPIMSelir0ZgiVXSh5.hGPb3RjQqbH3pv0Rr_2dQ59OIQ8;
|
||||||
|
path=/; expires=Tue, 11-Nov-25 20:11:25 GMT; domain=.api.openai.com; HttpOnly;
|
||||||
|
Secure; SameSite=None
|
||||||
|
- _cfuvid=u.Z6xV9tQd3ucK35BinKtlCkewcI6q_uQicyeEeeR18-1762890085355-0.0.1.1-604800000;
|
||||||
|
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||||
|
Strict-Transport-Security:
|
||||||
|
- max-age=31536000; includeSubDomains; preload
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
access-control-expose-headers:
|
||||||
|
- X-Request-ID
|
||||||
|
alt-svc:
|
||||||
|
- h3=":443"; ma=86400
|
||||||
|
cf-cache-status:
|
||||||
|
- DYNAMIC
|
||||||
|
openai-organization:
|
||||||
|
- crewai-iuxna1
|
||||||
|
openai-processing-ms:
|
||||||
|
- '559'
|
||||||
|
openai-project:
|
||||||
|
- proj_xitITlrFeen7zjNSzML82h9x
|
||||||
|
openai-version:
|
||||||
|
- '2020-10-01'
|
||||||
|
x-envoy-upstream-service-time:
|
||||||
|
- '735'
|
||||||
|
x-openai-proxy-wasm:
|
||||||
|
- v0.1
|
||||||
|
x-ratelimit-limit-project-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-limit-requests:
|
||||||
|
- '30000'
|
||||||
|
x-ratelimit-limit-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-remaining-project-tokens:
|
||||||
|
- '149999817'
|
||||||
|
x-ratelimit-remaining-requests:
|
||||||
|
- '29999'
|
||||||
|
x-ratelimit-remaining-tokens:
|
||||||
|
- '149999817'
|
||||||
|
x-ratelimit-reset-project-tokens:
|
||||||
|
- 0s
|
||||||
|
x-ratelimit-reset-requests:
|
||||||
|
- 2ms
|
||||||
|
x-ratelimit-reset-tokens:
|
||||||
|
- 0s
|
||||||
|
x-request-id:
|
||||||
|
- req_bcaa0f8500714ed09f967488b238ce2e
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: '{"trace_id": "aeb82647-004a-4a30-9481-d55f476d5659", "execution_type":
|
||||||
|
"crew", "user_identifier": null, "execution_context": {"crew_fingerprint": null,
|
||||||
|
"crew_name": "Unknown Crew", "flow_name": null, "crewai_version": "1.4.1", "privacy_level":
|
||||||
|
"standard"}, "execution_metadata": {"expected_duration_estimate": 300, "agent_count":
|
||||||
|
0, "task_count": 0, "flow_method_count": 0, "execution_started_at": "2025-11-11T19:45:17.648657+00:00"}}'
|
||||||
|
headers:
|
||||||
|
Accept:
|
||||||
|
- '*/*'
|
||||||
|
Accept-Encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Length:
|
||||||
|
- '434'
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
User-Agent:
|
||||||
|
- CrewAI-CLI/1.4.1
|
||||||
|
X-Crewai-Version:
|
||||||
|
- 1.4.1
|
||||||
|
method: POST
|
||||||
|
uri: https://app.crewai.com/crewai_plus/api/v1/tracing/batches
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: '{"error":"bad_credentials","message":"Bad credentials"}'
|
||||||
|
headers:
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Length:
|
||||||
|
- '55'
|
||||||
|
Content-Type:
|
||||||
|
- application/json; charset=utf-8
|
||||||
|
Date:
|
||||||
|
- Tue, 11 Nov 2025 19:45:17 GMT
|
||||||
|
cache-control:
|
||||||
|
- no-store
|
||||||
|
content-security-policy:
|
||||||
|
- 'default-src ''self'' *.app.crewai.com app.crewai.com; script-src ''self''
|
||||||
|
''unsafe-inline'' *.app.crewai.com app.crewai.com https://cdn.jsdelivr.net/npm/apexcharts
|
||||||
|
https://www.gstatic.com https://run.pstmn.io https://apis.google.com https://apis.google.com/js/api.js
|
||||||
|
https://accounts.google.com https://accounts.google.com/gsi/client https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css.map
|
||||||
|
https://*.google.com https://docs.google.com https://slides.google.com https://js.hs-scripts.com
|
||||||
|
https://js.sentry-cdn.com https://browser.sentry-cdn.com https://www.googletagmanager.com
|
||||||
|
https://js-na1.hs-scripts.com https://js.hubspot.com http://js-na1.hs-scripts.com
|
||||||
|
https://bat.bing.com https://cdn.amplitude.com https://cdn.segment.com https://d1d3n03t5zntha.cloudfront.net/
|
||||||
|
https://descriptusercontent.com https://edge.fullstory.com https://googleads.g.doubleclick.net
|
||||||
|
https://js.hs-analytics.net https://js.hs-banner.com https://js.hsadspixel.net
|
||||||
|
https://js.hscollectedforms.net https://js.usemessages.com https://snap.licdn.com
|
||||||
|
https://static.cloudflareinsights.com https://static.reo.dev https://www.google-analytics.com
|
||||||
|
https://share.descript.com/; style-src ''self'' ''unsafe-inline'' *.app.crewai.com
|
||||||
|
app.crewai.com https://cdn.jsdelivr.net/npm/apexcharts; img-src ''self'' data:
|
||||||
|
*.app.crewai.com app.crewai.com https://zeus.tools.crewai.com https://dashboard.tools.crewai.com
|
||||||
|
https://cdn.jsdelivr.net https://forms.hsforms.com https://track.hubspot.com
|
||||||
|
https://px.ads.linkedin.com https://px4.ads.linkedin.com https://www.google.com
|
||||||
|
https://www.google.com.br; font-src ''self'' data: *.app.crewai.com app.crewai.com;
|
||||||
|
connect-src ''self'' *.app.crewai.com app.crewai.com https://zeus.tools.crewai.com
|
||||||
|
https://connect.useparagon.com/ https://zeus.useparagon.com/* https://*.useparagon.com/*
|
||||||
|
https://run.pstmn.io https://connect.tools.crewai.com/ https://*.sentry.io
|
||||||
|
https://www.google-analytics.com https://edge.fullstory.com https://rs.fullstory.com
|
||||||
|
https://api.hubspot.com https://forms.hscollectedforms.net https://api.hubapi.com
|
||||||
|
https://px.ads.linkedin.com https://px4.ads.linkedin.com https://google.com/pagead/form-data/16713662509
|
||||||
|
https://google.com/ccm/form-data/16713662509 https://www.google.com/ccm/collect
|
||||||
|
https://worker-actionkit.tools.crewai.com https://api.reo.dev; frame-src ''self''
|
||||||
|
*.app.crewai.com app.crewai.com https://connect.useparagon.com/ https://zeus.tools.crewai.com
|
||||||
|
https://zeus.useparagon.com/* https://connect.tools.crewai.com/ https://docs.google.com
|
||||||
|
https://drive.google.com https://slides.google.com https://accounts.google.com
|
||||||
|
https://*.google.com https://app.hubspot.com/ https://td.doubleclick.net https://www.googletagmanager.com/
|
||||||
|
https://www.youtube.com https://share.descript.com'
|
||||||
|
expires:
|
||||||
|
- '0'
|
||||||
|
permissions-policy:
|
||||||
|
- camera=(), microphone=(self), geolocation=()
|
||||||
|
pragma:
|
||||||
|
- no-cache
|
||||||
|
referrer-policy:
|
||||||
|
- strict-origin-when-cross-origin
|
||||||
|
strict-transport-security:
|
||||||
|
- max-age=63072000; includeSubDomains
|
||||||
|
vary:
|
||||||
|
- Accept
|
||||||
|
x-content-type-options:
|
||||||
|
- nosniff
|
||||||
|
x-frame-options:
|
||||||
|
- SAMEORIGIN
|
||||||
|
x-permitted-cross-domain-policies:
|
||||||
|
- none
|
||||||
|
x-request-id:
|
||||||
|
- 48a89b0d-206b-4c1b-aa0d-ecc3b4ab525c
|
||||||
|
x-runtime:
|
||||||
|
- '0.088251'
|
||||||
|
x-xss-protection:
|
||||||
|
- 1; mode=block
|
||||||
|
status:
|
||||||
|
code: 401
|
||||||
|
message: Unauthorized
|
||||||
|
- request:
|
||||||
|
body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour
|
||||||
|
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||||
|
respond using the exact following format:\n\nThought: I now can give a great
|
||||||
|
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||||
|
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||||
|
depends on it!"},{"role":"user","content":"\nCurrent Task: Count to 3, taking
|
||||||
|
your time\n\nThis is the expected criteria for your final answer: A count\nyou
|
||||||
|
MUST return the actual complete content as the final answer, not a summary.\n\nBegin!
|
||||||
|
This is VERY important to you, use the tools available and give your best Final
|
||||||
|
Answer, your job depends on it!\n\nThought:"}],"model":"gpt-4.1-mini"}'
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- application/json
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '790'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
host:
|
||||||
|
- api.openai.com
|
||||||
|
user-agent:
|
||||||
|
- OpenAI/Python 1.109.1
|
||||||
|
x-stainless-arch:
|
||||||
|
- arm64
|
||||||
|
x-stainless-async:
|
||||||
|
- 'false'
|
||||||
|
x-stainless-lang:
|
||||||
|
- python
|
||||||
|
x-stainless-os:
|
||||||
|
- MacOS
|
||||||
|
x-stainless-package-version:
|
||||||
|
- 1.109.1
|
||||||
|
x-stainless-read-timeout:
|
||||||
|
- '600'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
|
x-stainless-runtime:
|
||||||
|
- CPython
|
||||||
|
x-stainless-runtime-version:
|
||||||
|
- 3.13.3
|
||||||
|
method: POST
|
||||||
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: !!binary |
|
||||||
|
H4sIAAAAAAAAAwAAAP//jFJNa9wwEL37Vww6r43tOpuNb2nKQgslOSy0NA1mIo9tdWVJSHK2Jex/
|
||||||
|
L/J+2Ns20IuE5s0bzXszrxEAEzUrgfEOPe+NjO9Q41atP3/79GG7vX8QD0Xq15svX9/fUd+yRWDo
|
||||||
|
5x/E/YmVcN0bSV5odYC5JfQUqmbXy3x1k77LViPQ65pkoLXGx0WSxb1QIs7T/CpOizgrjvROC06O
|
||||||
|
lfAYAQC8jmdoVNX0k5WQLk6RnpzDllh5TgJgVssQYeiccB6VZ4sJ5Fp5UmPvm04PbedL+AhK74Cj
|
||||||
|
gla8ECC0QQCgcjuy39VaKJRwO75KuFeUJAlsdnq8OkuUzD+w1AwOg0o1SDkDUCntMbg0Sns6Ivuz
|
||||||
|
GKlbY/Wz+4PKGqGE6ypL6LQKjTuvDRvRfQTwNJo2XPjAjNW98ZXXWxq/y5ZH09g0rBl6cwS99ihn
|
||||||
|
8esTcFGvqsmjkG5mO+PIO6on6jQjHGqhZ0A0U/13N/+qfVAuVPs/5SeAczKe6spYqgW/VDylWQq7
|
||||||
|
/Fba2eWxYebIvghOlRdkwyRqanCQhwVj7pfz1FeNUC1ZY8VhyxpTFTxfXWXNapmzaB/9BgAA//8D
|
||||||
|
AL0LXHV0AwAA
|
||||||
|
headers:
|
||||||
|
CF-RAY:
|
||||||
|
- 99d04a06dc4d1949-SJC
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Tue, 11 Nov 2025 19:45:18 GMT
|
||||||
|
Server:
|
||||||
|
- cloudflare
|
||||||
|
Set-Cookie:
|
||||||
|
- __cf_bm=KnsnYxgmlpoHf.5TWnNgU30xb2tc0gK7SC2BbUkud2M-1762890318-1.0.1.1-3KeaQY59x5mY6n8DINELLaH9_b68w7W4ZZ0KeOknBHmQyDwx5qbtDonfYxOjsO_KykjtJLHpB0bsINSNEa9TrjNQHqUWTlRhldfTLenUG44;
|
||||||
|
path=/; expires=Tue, 11-Nov-25 20:15:18 GMT; domain=.api.openai.com; HttpOnly;
|
||||||
|
Secure; SameSite=None
|
||||||
|
- _cfuvid=ekC35NRP79GCMP.eTi_odl5.6DIsAeFEXKlanWUZOH4-1762890318589-0.0.1.1-604800000;
|
||||||
|
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||||
|
Strict-Transport-Security:
|
||||||
|
- max-age=31536000; includeSubDomains; preload
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
access-control-expose-headers:
|
||||||
|
- X-Request-ID
|
||||||
|
alt-svc:
|
||||||
|
- h3=":443"; ma=86400
|
||||||
|
cf-cache-status:
|
||||||
|
- DYNAMIC
|
||||||
|
openai-organization:
|
||||||
|
- crewai-iuxna1
|
||||||
|
openai-processing-ms:
|
||||||
|
- '598'
|
||||||
|
openai-project:
|
||||||
|
- proj_xitITlrFeen7zjNSzML82h9x
|
||||||
|
openai-version:
|
||||||
|
- '2020-10-01'
|
||||||
|
x-envoy-upstream-service-time:
|
||||||
|
- '632'
|
||||||
|
x-openai-proxy-wasm:
|
||||||
|
- v0.1
|
||||||
|
x-ratelimit-limit-project-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-limit-requests:
|
||||||
|
- '30000'
|
||||||
|
x-ratelimit-limit-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-remaining-project-tokens:
|
||||||
|
- '149999827'
|
||||||
|
x-ratelimit-remaining-requests:
|
||||||
|
- '29999'
|
||||||
|
x-ratelimit-remaining-tokens:
|
||||||
|
- '149999827'
|
||||||
|
x-ratelimit-reset-project-tokens:
|
||||||
|
- 0s
|
||||||
|
x-ratelimit-reset-requests:
|
||||||
|
- 2ms
|
||||||
|
x-ratelimit-reset-tokens:
|
||||||
|
- 0s
|
||||||
|
x-request-id:
|
||||||
|
- req_cb36cbe6c33b42a28675e8c6d9a36fe9
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour
|
||||||
|
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||||
|
respond using the exact following format:\n\nThought: I now can give a great
|
||||||
|
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||||
|
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||||
|
depends on it!"},{"role":"user","content":"\nCurrent Task: Say hello\n\nThis
|
||||||
|
is the expected criteria for your final answer: A greeting\nyou MUST return
|
||||||
|
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||||
|
is VERY important to you, use the tools available and give your best Final Answer,
|
||||||
|
your job depends on it!\n\nThought:"},{"role":"user","content":"Additional context:
|
||||||
|
This is a test modification."}],"model":"gpt-4.1-mini"}'
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- application/json
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '851'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
host:
|
||||||
|
- api.openai.com
|
||||||
|
user-agent:
|
||||||
|
- OpenAI/Python 1.109.1
|
||||||
|
x-stainless-arch:
|
||||||
|
- arm64
|
||||||
|
x-stainless-async:
|
||||||
|
- 'false'
|
||||||
|
x-stainless-lang:
|
||||||
|
- python
|
||||||
|
x-stainless-os:
|
||||||
|
- MacOS
|
||||||
|
x-stainless-package-version:
|
||||||
|
- 1.109.1
|
||||||
|
x-stainless-read-timeout:
|
||||||
|
- '600'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
|
x-stainless-runtime:
|
||||||
|
- CPython
|
||||||
|
x-stainless-runtime-version:
|
||||||
|
- 3.13.3
|
||||||
|
method: POST
|
||||||
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: !!binary |
|
||||||
|
H4sIAAAAAAAAAwAAAP//jFJdi9swEHz3r9jqOT5sk+RSvx2lJW1poXDQ0vYwirS21cpaIclJr0f+
|
||||||
|
+yE7F/s+Cn0xeGdnNLO7dwkAU5KVwETLg+isTt9w+rr/YESx27+93RaHVm4/ff7y8Vpcffv+ly0i
|
||||||
|
g3a/UIQH1oWgzmoMiswIC4c8YFTNL9fF5nWWbfIB6EiijrTGhnR5kaedMiotsmKVZss0X57oLSmB
|
||||||
|
npXwIwEAuBu+0aiR+IeVkC0eKh16zxtk5bkJgDnSscK498oHbgJbTKAgE9AM3q9b6ps2lPAeDB1A
|
||||||
|
cAON2iNwaGIA4MYf0P0075ThGq6GvxK2qDW9mks6rHvPYy7Taz0DuDEUeJzLEObmhBzP9jU11tHO
|
||||||
|
P6GyWhnl28oh92SiVR/IsgE9JgA3w5j6R8mZddTZUAX6jcNz+fpy1GPTembo6gQGClzP6pti8YJe
|
||||||
|
JTFwpf1s0Exw0aKcqNNWeC8VzYBklvq5m5e0x+TKNP8jPwFCoA0oK+tQKvE48dTmMF7vv9rOUx4M
|
||||||
|
M49urwRWQaGLm5BY816PJ8X8rQ/YVbUyDTrr1HhXta2Wotis8nqzLlhyTO4BAAD//wMAuV0QSWYD
|
||||||
|
AAA=
|
||||||
|
headers:
|
||||||
|
CF-RAY:
|
||||||
|
- 99d044428f103c35-SJC
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Tue, 11 Nov 2025 19:41:22 GMT
|
||||||
|
Server:
|
||||||
|
- cloudflare
|
||||||
|
Set-Cookie:
|
||||||
|
- __cf_bm=jp.mByP87tLw_KZOIh7lXZ9UMACecreCMNwHwtJmUvQ-1762890082-1.0.1.1-D76UWkvWlN8e0zlQpgSlSHjrhx3Rkh_r8bz4XKx8kljJt8s9Okre9bo7M62ewJNFK9O9iuHkADMKeAEwlsc4Hg0MsF2vt2Hu1J0xikSInv0;
|
||||||
|
path=/; expires=Tue, 11-Nov-25 20:11:22 GMT; domain=.api.openai.com; HttpOnly;
|
||||||
|
Secure; SameSite=None
|
||||||
|
- _cfuvid=pzTqogdMFPJY2.Yrj49LODdUKbD8UBctCWNyIZVsvK4-1762890082258-0.0.1.1-604800000;
|
||||||
|
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||||
|
Strict-Transport-Security:
|
||||||
|
- max-age=31536000; includeSubDomains; preload
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
access-control-expose-headers:
|
||||||
|
- X-Request-ID
|
||||||
|
alt-svc:
|
||||||
|
- h3=":443"; ma=86400
|
||||||
|
cf-cache-status:
|
||||||
|
- DYNAMIC
|
||||||
|
openai-organization:
|
||||||
|
- crewai-iuxna1
|
||||||
|
openai-processing-ms:
|
||||||
|
- '460'
|
||||||
|
openai-project:
|
||||||
|
- proj_xitITlrFeen7zjNSzML82h9x
|
||||||
|
openai-version:
|
||||||
|
- '2020-10-01'
|
||||||
|
x-envoy-upstream-service-time:
|
||||||
|
- '478'
|
||||||
|
x-openai-proxy-wasm:
|
||||||
|
- v0.1
|
||||||
|
x-ratelimit-limit-project-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-limit-requests:
|
||||||
|
- '30000'
|
||||||
|
x-ratelimit-limit-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-remaining-project-tokens:
|
||||||
|
- '149999817'
|
||||||
|
x-ratelimit-remaining-requests:
|
||||||
|
- '29999'
|
||||||
|
x-ratelimit-remaining-tokens:
|
||||||
|
- '149999820'
|
||||||
|
x-ratelimit-reset-project-tokens:
|
||||||
|
- 0s
|
||||||
|
x-ratelimit-reset-requests:
|
||||||
|
- 2ms
|
||||||
|
x-ratelimit-reset-tokens:
|
||||||
|
- 0s
|
||||||
|
x-request-id:
|
||||||
|
- req_3bda51e6d3e34f8cadcc12551dc29ab0
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour
|
||||||
|
personal goal is: Test goal\nYou ONLY have access to the following tools, and
|
||||||
|
should NEVER make up tools that are not listed here:\n\nTool Name: test_tool\nTool
|
||||||
|
Arguments: {}\nTool Description: A test tool.\n\nIMPORTANT: Use the following
|
||||||
|
format in your response:\n\n```\nThought: you should always think about what
|
||||||
|
to do\nAction: the action to take, only one name of [test_tool], just the name,
|
||||||
|
exactly as it''s written.\nAction Input: the input to the action, just a simple
|
||||||
|
JSON object, enclosed in curly braces, using \" to wrap keys and values.\nObservation:
|
||||||
|
the result of the action\n```\n\nOnce all necessary information is gathered,
|
||||||
|
return the following format:\n\n```\nThought: I now know the final answer\nFinal
|
||||||
|
Answer: the final answer to the original input question\n```"},{"role":"user","content":"\nCurrent
|
||||||
|
Task: Use the test tool\n\nThis is the expected criteria for your final answer:
|
||||||
|
Tool result\nyou MUST return the actual complete content as the final answer,
|
||||||
|
not a summary.\n\nBegin! This is VERY important to you, use the tools available
|
||||||
|
and give your best Final Answer, your job depends on it!\n\nThought:"},{"role":"user","content":"Additional
|
||||||
|
context: This is a test modification."}],"model":"gpt-4.1-mini"}'
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- application/json
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '1311'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
host:
|
||||||
|
- api.openai.com
|
||||||
|
user-agent:
|
||||||
|
- OpenAI/Python 1.109.1
|
||||||
|
x-stainless-arch:
|
||||||
|
- arm64
|
||||||
|
x-stainless-async:
|
||||||
|
- 'false'
|
||||||
|
x-stainless-lang:
|
||||||
|
- python
|
||||||
|
x-stainless-os:
|
||||||
|
- MacOS
|
||||||
|
x-stainless-package-version:
|
||||||
|
- 1.109.1
|
||||||
|
x-stainless-read-timeout:
|
||||||
|
- '600'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
|
x-stainless-runtime:
|
||||||
|
- CPython
|
||||||
|
x-stainless-runtime-version:
|
||||||
|
- 3.13.3
|
||||||
|
method: POST
|
||||||
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: !!binary |
|
||||||
|
H4sIAAAAAAAAA4xTy47bMAy85ysIneMgcbNp1reizwXaXrpAD83CVmTaViqLWolu2gb590LOw94+
|
||||||
|
gF504HBGw6F0mAAIXYoMhGokq9aZ5KWkz/vdTn14i/dv9h9f19tXi3dtun789H71U0wjg7Y7VHxh
|
||||||
|
zRS1ziBrsidYeZSMUXXxfJWub+fzddoDLZVoIq12nCxni6TVVifpPL1J5stksTzTG9IKg8jgywQA
|
||||||
|
4NCf0agt8bvIYD69VFoMQdYosmsTgPBkYkXIEHRgaVlMB1CRZbS996IoNva+oa5uOIM7CA11poQu
|
||||||
|
IHCDwBg4ZyIDTFAj90WPj532WIK2FflWxqGhIt+DlbbSgLRhj362sS9URLNB6FKCO+s6zuBw3Nii
|
||||||
|
KMb2PFZdkDEj2xkzAqS1xP11fTAPZ+R4jcJQ7Txtw29UUWmrQ5N7lIFsHDswOdGjxwnAQx959yRF
|
||||||
|
4Ty1Lnr+iv116Wp10hPDqgf02XkfgomlGbFuL6wnenmJLLUJo6UJJVWD5UAdNiy7UtMImIym/tPN
|
||||||
|
37RPk2tb/4/8ACiFjrHMncdSq6cTD20e40/4V9s15d6wCOi/aYU5a/RxEyVWsjOn5ynCj8DY5pW2
|
||||||
|
NXrn9emNVi5fqnR9s6jWq1RMjpNfAAAA//8DANALR4WyAwAA
|
||||||
|
headers:
|
||||||
|
CF-RAY:
|
||||||
|
- 99d044470bdeb976-SJC
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Tue, 11 Nov 2025 19:41:23 GMT
|
||||||
|
Server:
|
||||||
|
- cloudflare
|
||||||
|
Set-Cookie:
|
||||||
|
- __cf_bm=p01_b1BsQgwR2woMBWf1E0gJMDDl7pvqkEVHpHAsMJA-1762890083-1.0.1.1-u8iYLTTx0lmfSR1.CzuuYiHgt03yVVUMsBD8WgExXWm7ts.grUwM1ifj9p6xIz.HElrnQdfDSBD5Lv045aNr61YcB8WW3Vz33W9N0Gn0P3w;
|
||||||
|
path=/; expires=Tue, 11-Nov-25 20:11:23 GMT; domain=.api.openai.com; HttpOnly;
|
||||||
|
Secure; SameSite=None
|
||||||
|
- _cfuvid=2gUmBgxb3VydVYt8.t_P6bY8U_pS.a4KeYpZWDDYM9Q-1762890083295-0.0.1.1-604800000;
|
||||||
|
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||||
|
Strict-Transport-Security:
|
||||||
|
- max-age=31536000; includeSubDomains; preload
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
access-control-expose-headers:
|
||||||
|
- X-Request-ID
|
||||||
|
alt-svc:
|
||||||
|
- h3=":443"; ma=86400
|
||||||
|
cf-cache-status:
|
||||||
|
- DYNAMIC
|
||||||
|
openai-organization:
|
||||||
|
- crewai-iuxna1
|
||||||
|
openai-processing-ms:
|
||||||
|
- '729'
|
||||||
|
openai-project:
|
||||||
|
- proj_xitITlrFeen7zjNSzML82h9x
|
||||||
|
openai-version:
|
||||||
|
- '2020-10-01'
|
||||||
|
x-envoy-upstream-service-time:
|
||||||
|
- '759'
|
||||||
|
x-openai-proxy-wasm:
|
||||||
|
- v0.1
|
||||||
|
x-ratelimit-limit-project-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-limit-requests:
|
||||||
|
- '30000'
|
||||||
|
x-ratelimit-limit-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-remaining-project-tokens:
|
||||||
|
- '149999707'
|
||||||
|
x-ratelimit-remaining-requests:
|
||||||
|
- '29999'
|
||||||
|
x-ratelimit-remaining-tokens:
|
||||||
|
- '149999707'
|
||||||
|
x-ratelimit-reset-project-tokens:
|
||||||
|
- 0s
|
||||||
|
x-ratelimit-reset-requests:
|
||||||
|
- 2ms
|
||||||
|
x-ratelimit-reset-tokens:
|
||||||
|
- 0s
|
||||||
|
x-request-id:
|
||||||
|
- req_70c7033dbc5e4ced80d3fdcbcda2c675
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
- request:
|
||||||
|
body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour
|
||||||
|
personal goal is: Test goal\nYou ONLY have access to the following tools, and
|
||||||
|
should NEVER make up tools that are not listed here:\n\nTool Name: test_tool\nTool
|
||||||
|
Arguments: {}\nTool Description: A test tool.\n\nIMPORTANT: Use the following
|
||||||
|
format in your response:\n\n```\nThought: you should always think about what
|
||||||
|
to do\nAction: the action to take, only one name of [test_tool], just the name,
|
||||||
|
exactly as it''s written.\nAction Input: the input to the action, just a simple
|
||||||
|
JSON object, enclosed in curly braces, using \" to wrap keys and values.\nObservation:
|
||||||
|
the result of the action\n```\n\nOnce all necessary information is gathered,
|
||||||
|
return the following format:\n\n```\nThought: I now know the final answer\nFinal
|
||||||
|
Answer: the final answer to the original input question\n```"},{"role":"user","content":"\nCurrent
|
||||||
|
Task: Use the test tool\n\nThis is the expected criteria for your final answer:
|
||||||
|
Tool result\nyou MUST return the actual complete content as the final answer,
|
||||||
|
not a summary.\n\nBegin! This is VERY important to you, use the tools available
|
||||||
|
and give your best Final Answer, your job depends on it!\n\nThought:"},{"role":"user","content":"Additional
|
||||||
|
context: This is a test modification."},{"role":"assistant","content":"```\nThought:
|
||||||
|
I should use the test_tool to get the required information for the final answer.\nAction:
|
||||||
|
test_tool\nAction Input: {}\n```\nObservation: test result"},{"role":"user","content":"Additional
|
||||||
|
context: This is a test modification."}],"model":"gpt-4.1-mini"}'
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- application/json
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '1584'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
cookie:
|
||||||
|
- __cf_bm=p01_b1BsQgwR2woMBWf1E0gJMDDl7pvqkEVHpHAsMJA-1762890083-1.0.1.1-u8iYLTTx0lmfSR1.CzuuYiHgt03yVVUMsBD8WgExXWm7ts.grUwM1ifj9p6xIz.HElrnQdfDSBD5Lv045aNr61YcB8WW3Vz33W9N0Gn0P3w;
|
||||||
|
_cfuvid=2gUmBgxb3VydVYt8.t_P6bY8U_pS.a4KeYpZWDDYM9Q-1762890083295-0.0.1.1-604800000
|
||||||
|
host:
|
||||||
|
- api.openai.com
|
||||||
|
user-agent:
|
||||||
|
- OpenAI/Python 1.109.1
|
||||||
|
x-stainless-arch:
|
||||||
|
- arm64
|
||||||
|
x-stainless-async:
|
||||||
|
- 'false'
|
||||||
|
x-stainless-lang:
|
||||||
|
- python
|
||||||
|
x-stainless-os:
|
||||||
|
- MacOS
|
||||||
|
x-stainless-package-version:
|
||||||
|
- 1.109.1
|
||||||
|
x-stainless-read-timeout:
|
||||||
|
- '600'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
|
x-stainless-runtime:
|
||||||
|
- CPython
|
||||||
|
x-stainless-runtime-version:
|
||||||
|
- 3.13.3
|
||||||
|
method: POST
|
||||||
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: !!binary |
|
||||||
|
H4sIAAAAAAAAAwAAAP//jFLBbtQwEL3nKyyfN1WS3S5pbhRRKCeEkCpgq8RrTxJTxzb2pC1U++/I
|
||||||
|
TrtJoUhcLNlv3vN7M/OQEEKloBWhvGfIB6vSN8xc3b+/FG/P3rX8x9X+y8dfHy7O8fYra88/0VVg
|
||||||
|
mP134PjEOuFmsApQGj3B3AFDCKr5q21RnmVZuY7AYASoQOssppuTPB2klmmRFadptknzzSO9N5KD
|
||||||
|
pxX5lhBCyEM8g1Et4J5WJFs9vQzgPeuAVsciQqgzKrxQ5r30yDTS1QxyoxF09N40zU5/7s3Y9ViR
|
||||||
|
S6LNHbkJB/ZAWqmZIkz7O3A7fRFvr+OtIggeiQM/KtzppmmW+g7a0bMQUo9KLQCmtUEWmhSTXT8i
|
||||||
|
h2MWZTrrzN7/QaWt1NL3tQPmjQ6+PRpLI3pICLmOPRuftYFaZwaLNZobiN+t83LSo/OsZvQIokGm
|
||||||
|
Fqz1dvWCXi0AmVR+0XXKGe9BzNR5RGwU0iyAZJH6bzcvaU/Jpe7+R34GOAeLIGrrQEj+PPFc5iCs
|
||||||
|
8r/Kjl2OhqkHdys51CjBhUkIaNmopv2i/qdHGOpW6g6cdXJastbWG16Up3lbbguaHJLfAAAA//8D
|
||||||
|
AJW0fwtzAwAA
|
||||||
|
headers:
|
||||||
|
CF-RAY:
|
||||||
|
- 99d0444cbd6db976-SJC
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Tue, 11 Nov 2025 19:41:23 GMT
|
||||||
|
Server:
|
||||||
|
- cloudflare
|
||||||
|
Strict-Transport-Security:
|
||||||
|
- max-age=31536000; includeSubDomains; preload
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
access-control-expose-headers:
|
||||||
|
- X-Request-ID
|
||||||
|
alt-svc:
|
||||||
|
- h3=":443"; ma=86400
|
||||||
|
cf-cache-status:
|
||||||
|
- DYNAMIC
|
||||||
|
openai-organization:
|
||||||
|
- crewai-iuxna1
|
||||||
|
openai-processing-ms:
|
||||||
|
- '527'
|
||||||
|
openai-project:
|
||||||
|
- proj_xitITlrFeen7zjNSzML82h9x
|
||||||
|
openai-version:
|
||||||
|
- '2020-10-01'
|
||||||
|
x-envoy-upstream-service-time:
|
||||||
|
- '578'
|
||||||
|
x-openai-proxy-wasm:
|
||||||
|
- v0.1
|
||||||
|
x-ratelimit-limit-project-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-limit-requests:
|
||||||
|
- '30000'
|
||||||
|
x-ratelimit-limit-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-remaining-project-tokens:
|
||||||
|
- '149999655'
|
||||||
|
x-ratelimit-remaining-requests:
|
||||||
|
- '29999'
|
||||||
|
x-ratelimit-remaining-tokens:
|
||||||
|
- '149999655'
|
||||||
|
x-ratelimit-reset-project-tokens:
|
||||||
|
- 0s
|
||||||
|
x-ratelimit-reset-requests:
|
||||||
|
- 2ms
|
||||||
|
x-ratelimit-reset-tokens:
|
||||||
|
- 0s
|
||||||
|
x-request-id:
|
||||||
|
- req_6b1d84dcdde643cea5160e155ee624db
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: '{"name":"llama3.2:3b"}'
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- '*/*'
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '22'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
host:
|
||||||
|
- localhost:11434
|
||||||
|
user-agent:
|
||||||
|
- litellm/1.78.5
|
||||||
|
method: POST
|
||||||
|
uri: http://localhost:11434/api/show
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: '{"error":"model ''llama3.2:3b'' not found"}'
|
||||||
|
headers:
|
||||||
|
Content-Length:
|
||||||
|
- '41'
|
||||||
|
Content-Type:
|
||||||
|
- application/json; charset=utf-8
|
||||||
|
Date:
|
||||||
|
- Tue, 11 Nov 2025 19:41:28 GMT
|
||||||
|
status:
|
||||||
|
code: 404
|
||||||
|
message: Not Found
|
||||||
|
- request:
|
||||||
|
body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour
|
||||||
|
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||||
|
respond using the exact following format:\n\nThought: I now can give a great
|
||||||
|
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||||
|
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||||
|
depends on it!"},{"role":"user","content":"\nCurrent Task: Say hello\n\nThis
|
||||||
|
is the expected criteria for your final answer: A greeting\nyou MUST return
|
||||||
|
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||||
|
is VERY important to you, use the tools available and give your best Final Answer,
|
||||||
|
your job depends on it!\n\nThought:"},{"role":"user","content":"Additional context:
|
||||||
|
This is a test modification."}],"model":"gpt-4.1-mini"}'
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- application/json
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '851'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
host:
|
||||||
|
- api.openai.com
|
||||||
|
user-agent:
|
||||||
|
- OpenAI/Python 1.109.1
|
||||||
|
x-stainless-arch:
|
||||||
|
- arm64
|
||||||
|
x-stainless-async:
|
||||||
|
- 'false'
|
||||||
|
x-stainless-lang:
|
||||||
|
- python
|
||||||
|
x-stainless-os:
|
||||||
|
- MacOS
|
||||||
|
x-stainless-package-version:
|
||||||
|
- 1.109.1
|
||||||
|
x-stainless-read-timeout:
|
||||||
|
- '600'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
|
x-stainless-runtime:
|
||||||
|
- CPython
|
||||||
|
x-stainless-runtime-version:
|
||||||
|
- 3.13.3
|
||||||
|
method: POST
|
||||||
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: !!binary |
|
||||||
|
H4sIAAAAAAAAAwAAAP//jFLRbtQwEHzPVyx+vlRJmrte84KOSqgFCSFAqLRUkc/ZJAbHa9lOy6m6
|
||||||
|
f0dOrpe0gMRLpHh2Znd29jECYLJiBTDRci86o+ILTten/ccPFzyp398srz9/2/o3X/PN6btN84kt
|
||||||
|
AoO2P1D4J9aJoM4o9JL0CAuL3GNQTc9W2fo8SdbnA9BRhSrQGuPj/CSNO6llnCXZMk7yOM0P9Jak
|
||||||
|
QMcKuI0AAB6HbxhUV/iLFZAsnl46dI43yIpjEQCzpMIL485J57n2bDGBgrRHPcz+paW+aX0BV6Dp
|
||||||
|
AQTX0Mh7BA5NMABcuwe03/VbqbmCzfBXwCUqRa/g8sC4grEN7KgHTxXfvZ63s1j3jgfPuldqBnCt
|
||||||
|
yfOws8Ho3QHZH60paoylrXtBZbXU0rWlRe5IBxvOk2EDuo8A7oYV9s+2woylzvjS008c2qWrs1GP
|
||||||
|
TdFNaJYdQE+eqxlrTPGlXlmh51K5WQhMcNFiNVGnxHhfSZoB0cz1n9P8TXt0LnXzP/ITIAQaj1Vp
|
||||||
|
LFZSPHc8lVkMl/2vsuOWh4GZQ3svBZZeog1JVFjzXo3nxtzOeezKWuoGrbFyvLnalLnI1su0Xq8y
|
||||||
|
Fu2j3wAAAP//AwDurzwzggMAAA==
|
||||||
|
headers:
|
||||||
|
CF-RAY:
|
||||||
|
- 99d0446e698367ab-SJC
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Tue, 11 Nov 2025 19:41:30 GMT
|
||||||
|
Server:
|
||||||
|
- cloudflare
|
||||||
|
Set-Cookie:
|
||||||
|
- __cf_bm=b52crfzdOm5rh4aOc2LfM8aQKFI.ZL9WCZXaPBDdG5k-1762890090-1.0.1.1-T2xhtwX0vuEnMIb8NRgP4w3RRn1N1ZwSjuhKBob1vDLDmN7XhCKkoIg3IrlC9KEyhA65IGa5DWsHfmlRKKxqw6sIPA98BSO6E3wsTRspHw4;
|
||||||
|
path=/; expires=Tue, 11-Nov-25 20:11:30 GMT; domain=.api.openai.com; HttpOnly;
|
||||||
|
Secure; SameSite=None
|
||||||
|
- _cfuvid=0TH0Kjp_5t6yhwXKA1wlKBHaczp.TeWhM2A5t6by1sI-1762890090153-0.0.1.1-604800000;
|
||||||
|
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||||
|
Strict-Transport-Security:
|
||||||
|
- max-age=31536000; includeSubDomains; preload
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
access-control-expose-headers:
|
||||||
|
- X-Request-ID
|
||||||
|
alt-svc:
|
||||||
|
- h3=":443"; ma=86400
|
||||||
|
cf-cache-status:
|
||||||
|
- DYNAMIC
|
||||||
|
openai-organization:
|
||||||
|
- crewai-iuxna1
|
||||||
|
openai-processing-ms:
|
||||||
|
- '1049'
|
||||||
|
openai-project:
|
||||||
|
- proj_xitITlrFeen7zjNSzML82h9x
|
||||||
|
openai-version:
|
||||||
|
- '2020-10-01'
|
||||||
|
x-envoy-upstream-service-time:
|
||||||
|
- '1387'
|
||||||
|
x-openai-proxy-wasm:
|
||||||
|
- v0.1
|
||||||
|
x-ratelimit-limit-project-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-limit-requests:
|
||||||
|
- '30000'
|
||||||
|
x-ratelimit-limit-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-remaining-project-tokens:
|
||||||
|
- '149999817'
|
||||||
|
x-ratelimit-remaining-requests:
|
||||||
|
- '29999'
|
||||||
|
x-ratelimit-remaining-tokens:
|
||||||
|
- '149999817'
|
||||||
|
x-ratelimit-reset-project-tokens:
|
||||||
|
- 0s
|
||||||
|
x-ratelimit-reset-requests:
|
||||||
|
- 2ms
|
||||||
|
x-ratelimit-reset-tokens:
|
||||||
|
- 0s
|
||||||
|
x-request-id:
|
||||||
|
- req_4b132b998ed941b5b6a85ddbb36e2b65
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
||||||
182
lib/crewai/tests/cassettes/test_llm_call_hooks_with_crew.yaml
Normal file
182
lib/crewai/tests/cassettes/test_llm_call_hooks_with_crew.yaml
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: '{"messages":[{"role":"system","content":"You are Researcher. You are a
|
||||||
|
researcher\nYour personal goal is: Research topics\nTo give my best complete
|
||||||
|
final answer to the task respond using the exact following format:\n\nThought:
|
||||||
|
I now can give a great answer\nFinal Answer: Your final answer must be the great
|
||||||
|
and the most complete as possible, it must be outcome described.\n\nI MUST use
|
||||||
|
these formats, my job depends on it!"},{"role":"user","content":"\nCurrent Task:
|
||||||
|
Research AI frameworks\n\nThis is the expected criteria for your final answer:
|
||||||
|
A research summary\nyou MUST return the actual complete content as the final
|
||||||
|
answer, not a summary.\n\nYou MUST follow these instructions: \n - Include specific
|
||||||
|
examples and real-world case studies to enhance the credibility and depth of
|
||||||
|
the article ideas.\n - Incorporate mentions of notable companies, projects,
|
||||||
|
or tools relevant to each topic to provide concrete context.\n - Add diverse
|
||||||
|
viewpoints such as interviews with experts, users, or thought leaders to enrich
|
||||||
|
the narrative and lend authority.\n - Address ethical, social, and emotional
|
||||||
|
considerations explicitly to reflect a balanced and comprehensive analysis.\n
|
||||||
|
- Enhance the descriptions by including implications for future developments
|
||||||
|
and the potential impact on society.\n - Use more engaging and vivid language
|
||||||
|
that draws the reader into each topic''s nuances and importance.\n - Include
|
||||||
|
notes or summaries that contextualize each set of ideas in terms of relevance
|
||||||
|
and potential reader engagement.\n - In future tasks, focus on elaborating initial
|
||||||
|
outlines into more detailed and nuanced article proposals with richer content
|
||||||
|
and insights.\n\nBegin! This is VERY important to you, use the tools available
|
||||||
|
and give your best Final Answer, your job depends on it!\n\nThought:"},{"role":"user","content":"Additional
|
||||||
|
context: This is a test modification."}],"model":"gpt-4.1-mini"}'
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- application/json
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '1894'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
host:
|
||||||
|
- api.openai.com
|
||||||
|
user-agent:
|
||||||
|
- OpenAI/Python 1.109.1
|
||||||
|
x-stainless-arch:
|
||||||
|
- arm64
|
||||||
|
x-stainless-async:
|
||||||
|
- 'false'
|
||||||
|
x-stainless-lang:
|
||||||
|
- python
|
||||||
|
x-stainless-os:
|
||||||
|
- MacOS
|
||||||
|
x-stainless-package-version:
|
||||||
|
- 1.109.1
|
||||||
|
x-stainless-read-timeout:
|
||||||
|
- '600'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
|
x-stainless-runtime:
|
||||||
|
- CPython
|
||||||
|
x-stainless-runtime-version:
|
||||||
|
- 3.13.3
|
||||||
|
method: POST
|
||||||
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: !!binary |
|
||||||
|
H4sIAAAAAAAAA2RXTXPbOBK9z6/o8smpkrRJJjPJ6OZy4ownceKKnY+qzaUJNMmOQTQLACUrc5kf
|
||||||
|
sZf9e/NLtrpBycrsRWWTINh4/V6/xz9/Ajhhf7KGE9djccMYlucoX57duvj+fXn/+hrzz8/v3j7f
|
||||||
|
dL/35y9efDlZ6BPSfCNX9k+tnAxjoMIS622XCAvprk+e//r0xW+PH//22G4M4inoY91Yls9WT5YD
|
||||||
|
R14+ffz0l+XjZ8snz+bHe2FH+WQN//4JAOBP+9VCo6f7kzXYZnZloJyxo5P1YRHASZKgV04wZ84F
|
||||||
|
YzlZPNx0EgtFq/22l6nryxouIcoWHEboeEOA0OkBAGPeUvoaLzhigDP7b/01fo0fKBMm18PNNAyY
|
||||||
|
diARzi7hIuFAW0l3WRddxpLET05hWX+NZ6lwy44xwGUsFAJ3FB3B6dnlI2gPTwImgtITNOjuGokE
|
||||||
|
0oLCluwVnjYUZBwolgWMSTbsOXaQS5pcmRJ5oLjhJFFXZMDooYiEDKXHAhSxCQRpLp9SXgDFjiPZ
|
||||||
|
n7paW4mRKUMR8JS5iwsoCTnW+57GIDsY0PUcCQJhilqBdTYDtXpGiiXsVnDbcz68DPKMlecNZeBY
|
||||||
|
BAb8JkmP9XD+BSTCsNxKCh5wHAM7VAS10tKzw2BlZDEkncTMntJ+id6i+5FSAY6Zu77kBXAIUy66
|
||||||
|
JnYKLSdokqAHHkZ0RZtXyPVRgnS7w+5Uditt45MVvN9Q2jBttRVvCQ3xH9q9/hqXcEsxS7oIsoXT
|
||||||
|
1yJdoEdreF8bqA0dJBcYZZwCJpCR4jLLlBxB4CZhUsinTB5aSf8Pb4WexsOV1dH7/v7rvxnaQPes
|
||||||
|
3VWwuZDRATAE2ea5a8oJQJckZzi//pgX8Np+dfPb6495BbVu22/KVvnRqTgq41T4GQLf0bwabhPG
|
||||||
|
HLCQbTRfvO6lSIaeuz5YH4BLhuwwYMOBSwV6lC2llaJ3vbsVZcnpFRX81wU6akTuHq3hTZRtNFB0
|
||||||
|
A7+LOLAzmk7F2o4BuoRjvzjswRla3IiqgeMRAakoBfJhM8J6Rk/N1HV7mI0/rFhhgDYReRmUy/TA
|
||||||
|
0lp3Bq3VwEK/weioio7jXB4l2HBmibZvxDIlDBAwdhN2pGA6ylnfe/ru7fUjw+FsRNcTXH15RwVO
|
||||||
|
zwb8LvHRGm4MuFAhPohsMa/L0zhKKnrsXKpa96fh2FLSQbOAQBtK2JGHZgd1Z/hMDdwoux1lOD37
|
||||||
|
fPOo6j7whlSZS594QxFckMlDnldapVesTJK2wLl0kYtOzluRcMcFTs/f3b5R+ret4nRUcN4f5Ac2
|
||||||
|
w0iplTQoiNaePBK5fgE8YEeVoYXuyxFmC5gKB/5em3woxtpxLqlgRHvs5m43PnBU0oHDdog/zr4c
|
||||||
|
qfVchoYjZXg3Ddc72HLpAaciAxZ24Lk1LAsb7yrbrz8COkdhnkEL6GbwS0Ib/T9Q0Hb8UUQvicYr
|
||||||
|
jt4KPwtjjxcSvI2epyv4oMPwsw3Dc2XrTZk8Ux04vxOG0js1DC3liryNx8sBlcvrY+n2mKEh7WOa
|
||||||
|
bHBy3FvJPMzyLhcaMjgcrT3SAmEKO3VFRwk86UjZH3tM+jIbqCMW7SzIVJwMlBeQJ9cDZht6+9NV
|
||||||
|
f7DWesaGFM9EhaOMWHo1BeyiZK5dOZuKRBlkyvCJenaB8vpY3T3hhsMOaNCxVgl9SznUjn9sKOkB
|
||||||
|
66jPFFpjsRa7b8RCnaMkbqZqClJdp/CgxHQm2uWAd3sVRTLpRiomfx7UeqvcrWBNCNGA3YtpPUtz
|
||||||
|
nhRtwskfgTjbJRb44/pKUocRznvMtKgere9VLVOy1w+iPXZuSjplS2/phL1SsZ390cq4MdXAB3JV
|
||||||
|
kRLX/1TGu9s3R3OMYq/t9darYw1Ke4RImcWjhe9HwAJmeJwMwxT3Lr23l/2Qy7X2TDgEyhWsGxsF
|
||||||
|
Wjnsg9TahKgsPYiJlGVSiHX4B/PcPev0Paakg1w0NWwkTHqPv+vahh/s3KepA8/ZyYZSdfWfV/Cq
|
||||||
|
5gRjTKYEn5i2o3Asqi6NaOb5Jg8+jkyzbNMKLoiXF8TwlheAkEidijzcFIytJKu/pZwlWXmEmo/u
|
||||||
|
HReqRtzIVAx4T4M4nR/fK8bSqiJrbtswqhWnZZuYog+7H7JSMxVAcDhVZdqOesiWzer2NFOw9NaY
|
||||||
|
ZDRtFEpjojJb8QqubVjpJnpALEcx0E777tPly8sz1VuPmb/XhBppTitH6bWIdlCGQbxSlQZKZqw9
|
||||||
|
Jr/FyuLaX0lzE/UTYFCrnXJewas55J1dqqmKw0IZMGR5iBKqXUcp7kMt5t1xwJG2iq6dbJcZgsGe
|
||||||
|
pHt0lBotrWHMmkNbiFJAp2g7haDJNqpHGkueHepZwI0lzmpFrwaZk8f5DwF0/TVqTkg4sj/OBHNH
|
||||||
|
j3BSqj+0nbzettGrDVHYrbN6bJ4/I8hD4nyX//7rP1o64DCGg/r1W4A36HZASbJd0Dq/SaO8HwPW
|
||||||
|
OlZwIUlB0M+1BQRMFkPIEgP5PVbNxKHG4p4yHZc94A449pS4MndM9G3yFh5oaMj76saHBOKx4Ooh
|
||||||
|
KIN9GGrntTqltSJhX1x+KjtlD2tphplNnRE1vFQZt8gpUs7QkkapY6v5RxauZuMwefDiJstyB++a
|
||||||
|
fcQWqhK6OmzofgzIcZ9OTX0r6zqVHQxTLtBgsOHIMcqmbmgMTtRNYZ5/NrOqM3L0vGE/YYBUI7CF
|
||||||
|
twN3thTCsiHLMpRHUn4FxRcyxVwDVcsU/CzC/uD3Rs5fVnAxWch/+fBRWN9Rq7YsoF84ysu3Ijau
|
||||||
|
W0lbTH4B/wMAAP//jFjNbtswDL73KQSfWiAo0Kwbgt2GXVZgP6fdVgSKTNtcZUnQT7ocCuwh9oR7
|
||||||
|
koGkYjtZBuxMR7Ep8vs7nUdaTULQfaXDqTFqLDYjrbOd1dr1QrqSLKs6rVoJXVr0Kvse8gDxZnWU
|
||||||
|
UDRg577mmqj+08cb+SXbbogKHMT+MMlGc/SSIfoRaWvTqGOGuJqYkV6GZAvBhjAQupZcHwJtTBf9
|
||||||
|
qKAtsi2qpKo5E10E79/stEJgGFv4aG3V6B1mH2sHlbFIH6TMoF0PasSMvdAwacglcn4J4OilXKs+
|
||||||
|
VJNB5oYbbquXROJqndkXZ0/Xw7Y5ELbUj1rgG48c+UeeUbGCA1TPStJOXK20q/PFtW8XnWR9ShdV
|
||||||
|
ejoNWjWUUbsT8FnN6HP03HvsUYZfTIWxJeGeFsUM2lpwPbuCb+49xSs/Mg39Z59BIPFSDIDVHB5U
|
||||||
|
BAt77TJ39t2DCksyWqngLZrDqJ+mjIKQhzkMEsuEsrNobtUD4Sz7Dc38FWGgPdqDSk6HNPhcCcMW
|
||||||
|
gy0Ty+CfzxaB/c7Jhg9o2auNgbfaRMzckgidrWrOu2WuQAPcdWwx+GZqt4TYDan4JCp+GVeQ1iB8
|
||||||
|
fQIRztkHNBTO6MmYGpI/O8sa0fgSa0W5IhqOFU6J5GmXiYakA4IUc7jBHkB2XNQjaR5mVnnXSwBB
|
||||||
|
RPmdgJAP5yYwTP7++SsPcOBnqhzMh6NzDFZnkpVJpUGHGsEQuJMH8pSddWfB1q366lqIlNy1c2Rz
|
||||||
|
OqDz1MlITOty5E9MClJisya2Y9BMHtXuoFPP+lAVBIPfkeclWbIHtQMHEtjVqSM+YoFMm3q7zBRJ
|
||||||
|
OyRNwaYr1i4K2jkv1MNp5mOtvEz5pfV9iH6Xzn7adOgwDdsIOnlHWWXKPjRcfblS6pFz0nISfTYy
|
||||||
|
o9vsn4D/7tV9zUmbOZ+dq5vNplazz9rOhbv1+lg5OXHbQtZo0yJrbQyFFe382zmYZRJYFK4W3/33
|
||||||
|
+1w6W74dXf8/x88FYyBkaLezWbj0WAQav389NvWZX7ipnmebESLdRQudLlZS5UaM87ZD15OoRomW
|
||||||
|
u7C9N+vN67tu82bdXL1c/QEAAP//AwBbY8c8aRcAAA==
|
||||||
|
headers:
|
||||||
|
CF-RAY:
|
||||||
|
- 99d0447958ce36e8-SJC
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Tue, 11 Nov 2025 19:41:45 GMT
|
||||||
|
Server:
|
||||||
|
- cloudflare
|
||||||
|
Set-Cookie:
|
||||||
|
- __cf_bm=dSe1gQEFfPpE4AOsFi3S3RQkzPCQnV1.Ywe__K7cSSU-1762890105-1.0.1.1-I1CSTO8ri4tjbaHdIHQ9YP9c2pa.y9WwMQFRaUztT95T_OAe5V0ndTFN4pO1RiCXh15TUpWmBxRdxIWjcYDMqrDIvKWInLO5aavGFWZ1rys;
|
||||||
|
path=/; expires=Tue, 11-Nov-25 20:11:45 GMT; domain=.api.openai.com; HttpOnly;
|
||||||
|
Secure; SameSite=None
|
||||||
|
- _cfuvid=LMf_4EPFZGfTiqcjmjEk7WxOTuX2ukd3Cs_R8170wJ4-1762890105804-0.0.1.1-604800000;
|
||||||
|
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||||
|
Strict-Transport-Security:
|
||||||
|
- max-age=31536000; includeSubDomains; preload
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
access-control-expose-headers:
|
||||||
|
- X-Request-ID
|
||||||
|
alt-svc:
|
||||||
|
- h3=":443"; ma=86400
|
||||||
|
cf-cache-status:
|
||||||
|
- DYNAMIC
|
||||||
|
openai-organization:
|
||||||
|
- crewai-iuxna1
|
||||||
|
openai-processing-ms:
|
||||||
|
- '15065'
|
||||||
|
openai-project:
|
||||||
|
- proj_xitITlrFeen7zjNSzML82h9x
|
||||||
|
openai-version:
|
||||||
|
- '2020-10-01'
|
||||||
|
x-envoy-upstream-service-time:
|
||||||
|
- '15254'
|
||||||
|
x-openai-proxy-wasm:
|
||||||
|
- v0.1
|
||||||
|
x-ratelimit-limit-project-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-limit-requests:
|
||||||
|
- '30000'
|
||||||
|
x-ratelimit-limit-tokens:
|
||||||
|
- '150000000'
|
||||||
|
x-ratelimit-remaining-project-tokens:
|
||||||
|
- '149999560'
|
||||||
|
x-ratelimit-remaining-requests:
|
||||||
|
- '29999'
|
||||||
|
x-ratelimit-remaining-tokens:
|
||||||
|
- '149999560'
|
||||||
|
x-ratelimit-reset-project-tokens:
|
||||||
|
- 0s
|
||||||
|
x-ratelimit-reset-requests:
|
||||||
|
- 2ms
|
||||||
|
x-ratelimit-reset-tokens:
|
||||||
|
- 0s
|
||||||
|
x-request-id:
|
||||||
|
- req_c49c9fba20ff4f05903eff3c78797ce1
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
||||||
@@ -37,6 +37,36 @@ class TestOktaProvider:
|
|||||||
provider = OktaProvider(settings)
|
provider = OktaProvider(settings)
|
||||||
expected_url = "https://my-company.okta.com/oauth2/default/v1/device/authorize"
|
expected_url = "https://my-company.okta.com/oauth2/default/v1/device/authorize"
|
||||||
assert provider.get_authorize_url() == expected_url
|
assert provider.get_authorize_url() == expected_url
|
||||||
|
|
||||||
|
def test_get_authorize_url_with_custom_authorization_server_name(self):
|
||||||
|
settings = Oauth2Settings(
|
||||||
|
provider="okta",
|
||||||
|
domain="test-domain.okta.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
audience=None,
|
||||||
|
extra={
|
||||||
|
"using_org_auth_server": False,
|
||||||
|
"authorization_server_name": "my_auth_server_xxxAAA777"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = OktaProvider(settings)
|
||||||
|
expected_url = "https://test-domain.okta.com/oauth2/my_auth_server_xxxAAA777/v1/device/authorize"
|
||||||
|
assert provider.get_authorize_url() == expected_url
|
||||||
|
|
||||||
|
def test_get_authorize_url_when_using_org_auth_server(self):
|
||||||
|
settings = Oauth2Settings(
|
||||||
|
provider="okta",
|
||||||
|
domain="test-domain.okta.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
audience=None,
|
||||||
|
extra={
|
||||||
|
"using_org_auth_server": True,
|
||||||
|
"authorization_server_name": None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = OktaProvider(settings)
|
||||||
|
expected_url = "https://test-domain.okta.com/oauth2/v1/device/authorize"
|
||||||
|
assert provider.get_authorize_url() == expected_url
|
||||||
|
|
||||||
def test_get_token_url(self):
|
def test_get_token_url(self):
|
||||||
expected_url = "https://test-domain.okta.com/oauth2/default/v1/token"
|
expected_url = "https://test-domain.okta.com/oauth2/default/v1/token"
|
||||||
@@ -53,6 +83,36 @@ class TestOktaProvider:
|
|||||||
expected_url = "https://another-domain.okta.com/oauth2/default/v1/token"
|
expected_url = "https://another-domain.okta.com/oauth2/default/v1/token"
|
||||||
assert provider.get_token_url() == expected_url
|
assert provider.get_token_url() == expected_url
|
||||||
|
|
||||||
|
def test_get_token_url_with_custom_authorization_server_name(self):
|
||||||
|
settings = Oauth2Settings(
|
||||||
|
provider="okta",
|
||||||
|
domain="test-domain.okta.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
audience=None,
|
||||||
|
extra={
|
||||||
|
"using_org_auth_server": False,
|
||||||
|
"authorization_server_name": "my_auth_server_xxxAAA777"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = OktaProvider(settings)
|
||||||
|
expected_url = "https://test-domain.okta.com/oauth2/my_auth_server_xxxAAA777/v1/token"
|
||||||
|
assert provider.get_token_url() == expected_url
|
||||||
|
|
||||||
|
def test_get_token_url_when_using_org_auth_server(self):
|
||||||
|
settings = Oauth2Settings(
|
||||||
|
provider="okta",
|
||||||
|
domain="test-domain.okta.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
audience=None,
|
||||||
|
extra={
|
||||||
|
"using_org_auth_server": True,
|
||||||
|
"authorization_server_name": None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = OktaProvider(settings)
|
||||||
|
expected_url = "https://test-domain.okta.com/oauth2/v1/token"
|
||||||
|
assert provider.get_token_url() == expected_url
|
||||||
|
|
||||||
def test_get_jwks_url(self):
|
def test_get_jwks_url(self):
|
||||||
expected_url = "https://test-domain.okta.com/oauth2/default/v1/keys"
|
expected_url = "https://test-domain.okta.com/oauth2/default/v1/keys"
|
||||||
assert self.provider.get_jwks_url() == expected_url
|
assert self.provider.get_jwks_url() == expected_url
|
||||||
@@ -68,6 +128,36 @@ class TestOktaProvider:
|
|||||||
expected_url = "https://dev.okta.com/oauth2/default/v1/keys"
|
expected_url = "https://dev.okta.com/oauth2/default/v1/keys"
|
||||||
assert provider.get_jwks_url() == expected_url
|
assert provider.get_jwks_url() == expected_url
|
||||||
|
|
||||||
|
def test_get_jwks_url_with_custom_authorization_server_name(self):
|
||||||
|
settings = Oauth2Settings(
|
||||||
|
provider="okta",
|
||||||
|
domain="test-domain.okta.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
audience=None,
|
||||||
|
extra={
|
||||||
|
"using_org_auth_server": False,
|
||||||
|
"authorization_server_name": "my_auth_server_xxxAAA777"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = OktaProvider(settings)
|
||||||
|
expected_url = "https://test-domain.okta.com/oauth2/my_auth_server_xxxAAA777/v1/keys"
|
||||||
|
assert provider.get_jwks_url() == expected_url
|
||||||
|
|
||||||
|
def test_get_jwks_url_when_using_org_auth_server(self):
|
||||||
|
settings = Oauth2Settings(
|
||||||
|
provider="okta",
|
||||||
|
domain="test-domain.okta.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
audience=None,
|
||||||
|
extra={
|
||||||
|
"using_org_auth_server": True,
|
||||||
|
"authorization_server_name": None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = OktaProvider(settings)
|
||||||
|
expected_url = "https://test-domain.okta.com/oauth2/v1/keys"
|
||||||
|
assert provider.get_jwks_url() == expected_url
|
||||||
|
|
||||||
def test_get_issuer(self):
|
def test_get_issuer(self):
|
||||||
expected_issuer = "https://test-domain.okta.com/oauth2/default"
|
expected_issuer = "https://test-domain.okta.com/oauth2/default"
|
||||||
assert self.provider.get_issuer() == expected_issuer
|
assert self.provider.get_issuer() == expected_issuer
|
||||||
@@ -83,6 +173,36 @@ class TestOktaProvider:
|
|||||||
expected_issuer = "https://prod.okta.com/oauth2/default"
|
expected_issuer = "https://prod.okta.com/oauth2/default"
|
||||||
assert provider.get_issuer() == expected_issuer
|
assert provider.get_issuer() == expected_issuer
|
||||||
|
|
||||||
|
def test_get_issuer_with_custom_authorization_server_name(self):
|
||||||
|
settings = Oauth2Settings(
|
||||||
|
provider="okta",
|
||||||
|
domain="test-domain.okta.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
audience=None,
|
||||||
|
extra={
|
||||||
|
"using_org_auth_server": False,
|
||||||
|
"authorization_server_name": "my_auth_server_xxxAAA777"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = OktaProvider(settings)
|
||||||
|
expected_issuer = "https://test-domain.okta.com/oauth2/my_auth_server_xxxAAA777"
|
||||||
|
assert provider.get_issuer() == expected_issuer
|
||||||
|
|
||||||
|
def test_get_issuer_when_using_org_auth_server(self):
|
||||||
|
settings = Oauth2Settings(
|
||||||
|
provider="okta",
|
||||||
|
domain="test-domain.okta.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
audience=None,
|
||||||
|
extra={
|
||||||
|
"using_org_auth_server": True,
|
||||||
|
"authorization_server_name": None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = OktaProvider(settings)
|
||||||
|
expected_issuer = "https://test-domain.okta.com"
|
||||||
|
assert provider.get_issuer() == expected_issuer
|
||||||
|
|
||||||
def test_get_audience(self):
|
def test_get_audience(self):
|
||||||
assert self.provider.get_audience() == "test-audience"
|
assert self.provider.get_audience() == "test-audience"
|
||||||
|
|
||||||
@@ -100,3 +220,38 @@ class TestOktaProvider:
|
|||||||
|
|
||||||
def test_get_client_id(self):
|
def test_get_client_id(self):
|
||||||
assert self.provider.get_client_id() == "test-client-id"
|
assert self.provider.get_client_id() == "test-client-id"
|
||||||
|
|
||||||
|
def test_get_required_fields(self):
|
||||||
|
assert set(self.provider.get_required_fields()) == set(["authorization_server_name", "using_org_auth_server"])
|
||||||
|
|
||||||
|
def test_oauth2_base_url(self):
|
||||||
|
assert self.provider._oauth2_base_url() == "https://test-domain.okta.com/oauth2/default"
|
||||||
|
|
||||||
|
def test_oauth2_base_url_with_custom_authorization_server_name(self):
|
||||||
|
settings = Oauth2Settings(
|
||||||
|
provider="okta",
|
||||||
|
domain="test-domain.okta.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
audience=None,
|
||||||
|
extra={
|
||||||
|
"using_org_auth_server": False,
|
||||||
|
"authorization_server_name": "my_auth_server_xxxAAA777"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = OktaProvider(settings)
|
||||||
|
assert provider._oauth2_base_url() == "https://test-domain.okta.com/oauth2/my_auth_server_xxxAAA777"
|
||||||
|
|
||||||
|
def test_oauth2_base_url_when_using_org_auth_server(self):
|
||||||
|
settings = Oauth2Settings(
|
||||||
|
provider="okta",
|
||||||
|
domain="test-domain.okta.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
audience=None,
|
||||||
|
extra={
|
||||||
|
"using_org_auth_server": True,
|
||||||
|
"authorization_server_name": None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider = OktaProvider(settings)
|
||||||
|
assert provider._oauth2_base_url() == "https://test-domain.okta.com/oauth2"
|
||||||
@@ -37,7 +37,8 @@ class TestEnterpriseConfigureCommand(unittest.TestCase):
|
|||||||
'audience': 'test_audience',
|
'audience': 'test_audience',
|
||||||
'domain': 'test.domain.com',
|
'domain': 'test.domain.com',
|
||||||
'device_authorization_client_id': 'test_client_id',
|
'device_authorization_client_id': 'test_client_id',
|
||||||
'provider': 'workos'
|
'provider': 'workos',
|
||||||
|
'extra': {}
|
||||||
}
|
}
|
||||||
mock_requests_get.return_value = mock_response
|
mock_requests_get.return_value = mock_response
|
||||||
|
|
||||||
@@ -60,11 +61,12 @@ class TestEnterpriseConfigureCommand(unittest.TestCase):
|
|||||||
('oauth2_provider', 'workos'),
|
('oauth2_provider', 'workos'),
|
||||||
('oauth2_audience', 'test_audience'),
|
('oauth2_audience', 'test_audience'),
|
||||||
('oauth2_client_id', 'test_client_id'),
|
('oauth2_client_id', 'test_client_id'),
|
||||||
('oauth2_domain', 'test.domain.com')
|
('oauth2_domain', 'test.domain.com'),
|
||||||
|
('oauth2_extra', {})
|
||||||
]
|
]
|
||||||
|
|
||||||
actual_calls = self.mock_settings_command.set.call_args_list
|
actual_calls = self.mock_settings_command.set.call_args_list
|
||||||
self.assertEqual(len(actual_calls), 5)
|
self.assertEqual(len(actual_calls), 6)
|
||||||
|
|
||||||
for i, (key, value) in enumerate(expected_calls):
|
for i, (key, value) in enumerate(expected_calls):
|
||||||
call_args = actual_calls[i][0]
|
call_args = actual_calls[i][0]
|
||||||
|
|||||||
Reference in New Issue
Block a user