From e375d8b6063743bd6214494f18f455032b7c9c72 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 8 Dec 2025 09:06:57 +0100 Subject: [PATCH 1/3] Updated doc for configuration model --- src/models/config.py | 271 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 233 insertions(+), 38 deletions(-) diff --git a/src/models/config.py b/src/models/config.py index d8703ca67..8958f18f1 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -72,7 +72,14 @@ class TLSConfiguration(ConfigurationBase): @model_validator(mode="after") def check_tls_configuration(self) -> Self: - """Check TLS configuration.""" + """ + Perform post-validation checks for the TLS configuration. + + Currently a no-op that returns the model unchanged. + + Returns: + Self: The validated model instance. + """ return self @@ -125,7 +132,16 @@ class CORSConfiguration(ConfigurationBase): @model_validator(mode="after") def check_cors_configuration(self) -> Self: - """Check CORS configuration.""" + """ + Validate CORS settings and enforce that credentials are not allowed. + + Raises: + ValueError: If `allow_credentials` is true while + `allow_origins` contains the "*" wildcard. + + Returns: + Self: The validated configuration instance. + """ # credentials are not allowed with wildcard origins per CORS/Fetch spec. # see https://fastapi.tiangolo.com/tutorial/cors/ if self.allow_credentials and "*" in self.allow_origins: @@ -160,9 +176,9 @@ class InMemoryCacheConfig(ConfigurationBase): class PostgreSQLDatabaseConfiguration(ConfigurationBase): """PostgreSQL database configuration. - PostgreSQL database is used by Lightspeed Core Stack service for storing information about - conversation IDs. It can also be leveraged to store conversation history and information - about quota usage. + PostgreSQL database is used by Lightspeed Core Stack service for storing + information about conversation IDs. It can also be leveraged to store + conversation history and information about quota usage. Useful resources: @@ -228,7 +244,17 @@ class PostgreSQLDatabaseConfiguration(ConfigurationBase): @model_validator(mode="after") def check_postgres_configuration(self) -> Self: - """Check PostgreSQL configuration.""" + """ + Validate PostgreSQL configuration constraints. + + Ensures the configured port is within the valid TCP port range. + + Returns: + self: The validated configuration instance. + + Raises: + ValueError: If `port` is greater than 65535. + """ if self.port > 65535: raise ValueError("Port value should be less than 65536") return self @@ -251,7 +277,17 @@ class DatabaseConfiguration(ConfigurationBase): @model_validator(mode="after") def check_database_configuration(self) -> Self: - """Check that exactly one database type is configured.""" + """ + Ensure exactly one database backend is configured, defaulting to a temporary SQLite one. + + If neither `sqlite` nor `postgres` is set, assigns a default SQLite + configuration using the file path "/tmp/lightspeed-stack.db". If both + backends are configured, raises a `ValueError`. + + Returns: + self: The configuration instance with exactly one + database backend set. + """ total_configured_dbs = sum([self.sqlite is not None, self.postgres is not None]) if total_configured_dbs == 0: @@ -267,7 +303,17 @@ def check_database_configuration(self) -> Self: @property def db_type(self) -> Literal["sqlite", "postgres"]: - """Return the configured database type.""" + """ + Determine which database backend is configured. + + Returns: + The string "sqlite" if a SQLite configuration is + present, "postgres" if a PostgreSQL configuration is + present. + + Raises: + ValueError: If neither SQLite nor PostgreSQL configuration is set. + """ if self.sqlite is not None: return "sqlite" if self.postgres is not None: @@ -276,7 +322,18 @@ def db_type(self) -> Literal["sqlite", "postgres"]: @property def config(self) -> SQLiteDatabaseConfiguration | PostgreSQLDatabaseConfiguration: - """Return the active database configuration.""" + """ + Get the active database backend configuration. + + Returns: + SQLiteDatabaseConfiguration or PostgreSQLDatabaseConfiguration: The + configured SQLite configuration if `sqlite` is set, otherwise the + configured PostgreSQL configuration if `postgres` is set. + + Raises: + ValueError: If neither `sqlite` nor `postgres` is + configured. + """ if self.sqlite is not None: return self.sqlite if self.postgres is not None: @@ -287,10 +344,10 @@ def config(self) -> SQLiteDatabaseConfiguration | PostgreSQLDatabaseConfiguratio class ServiceConfiguration(ConfigurationBase): """Service configuration. - Lightspeed Core Stack is a REST API service that accepts requests - on a specified hostname and port. It is also possible to enable - authentication and specify the number of Uvicorn workers. When more - workers are specified, the service can handle requests concurrently. + Lightspeed Core Stack is a REST API service that accepts requests on a + specified hostname and port. It is also possible to enable authentication + and specify the number of Uvicorn workers. When more workers are specified, + the service can handle requests concurrently. """ host: str = Field( @@ -350,7 +407,15 @@ class ServiceConfiguration(ConfigurationBase): @model_validator(mode="after") def check_service_configuration(self) -> Self: - """Check service configuration.""" + """ + Validate service configuration and enforce allowed port range. + + Raises: + ValueError: If `port` is greater than 65535. + + Returns: + self: The validated model instance. + """ if self.port > 65535: raise ValueError("Port value should be less than 65536") return self @@ -359,11 +424,11 @@ def check_service_configuration(self) -> Self: class ModelContextProtocolServer(ConfigurationBase): """Model context protocol server configuration. - MCP (Model Context Protocol) servers provide tools and - capabilities to the AI agents. These are configured by this structure. - Only MCP servers defined in the lightspeed-stack.yaml configuration are - available to the agents. Tools configured in the llama-stack run.yaml - are not accessible to lightspeed-core agents. + MCP (Model Context Protocol) servers provide tools and capabilities to the + AI agents. These are configured by this structure. Only MCP servers + defined in the lightspeed-stack.yaml configuration are available to the + agents. Tools configured in the llama-stack run.yaml are not accessible to + lightspeed-core agents. Useful resources: @@ -434,16 +499,22 @@ class LlamaStackConfiguration(ConfigurationBase): @model_validator(mode="after") def check_llama_stack_model(self) -> Self: """ - Validate the Llama stack configuration after model initialization. + Validate the Llama Stack configuration and enforce mode-specific requirements. - Ensures that either a URL is provided for server mode or library client - mode is explicitly enabled. If library client mode is enabled, verifies - that a configuration file path is specified and points to an existing, - readable file. Raises a ValueError if any required condition is not - met. + If no URL is provided, requires explicit library-client mode selection. + When library-client mode is enabled, requires a non-empty + `library_client_config_path` that points to a regular, readable YAML + file (checked via checks.file_check). Also normalizes a None + `use_as_library_client` to False. Returns: Self: The validated LlamaStackConfiguration instance. + + Raises: + ValueError: If the configuration is invalid, e.g. no + URL and library-client mode is unspecified or + disabled, or library-client mode is enabled but + `library_client_config_path` is not provided. """ if self.url is None: # when URL is not set, it is supposed that Llama Stack should be run in library mode @@ -510,7 +581,18 @@ class UserDataCollection(ConfigurationBase): @model_validator(mode="after") def check_storage_location_is_set_when_needed(self) -> Self: - """Ensure storage directories are set when feedback or transcripts are enabled.""" + """ + Ensure storage locations are configured and writable when feedback is enabled. + + If feedback collection is enabled, `feedback_storage` must be provided + and refer to a directory that exists or can be created and is writable. + If transcript collection is enabled, `transcripts_storage` must be + provided and refer to a directory that exists or can be created and is + writable. + + Returns: + self: The validated UserDataCollection instance. + """ if self.feedback_enabled: if self.feedback_storage is None: raise ValueError( @@ -583,7 +665,18 @@ class JwtRoleRule(ConfigurationBase): @model_validator(mode="after") def check_jsonpath(self) -> Self: - """Verify that the JSONPath expression is valid.""" + """ + Validate that the `jsonpath` expression parses as a JSONPath. + + Returns: + self: The same model instance when the JSONPath + expression is valid. + + Raises: + ValueError: If the `jsonpath` cannot be parsed; the + message includes the offending expression and parser + error. + """ try: # try to parse the JSONPath jsonpath_ng.parse(self.jsonpath) @@ -595,7 +688,16 @@ def check_jsonpath(self) -> Self: @model_validator(mode="after") def check_roles(self) -> Self: - """Ensure that at least one role is specified.""" + """ + Validate the rule's roles list and enforce required constraints. + + Performs three checks: ensures at least one role is present, enforces + that all roles are unique, and prohibits the wildcard role `"*"`. + + Returns: + self: The same `JwtRoleRule` instance when validation + succeeds. + """ if not self.roles: raise ValueError("At least one role must be specified in the rule") @@ -612,7 +714,16 @@ def check_roles(self) -> Self: @model_validator(mode="after") def check_regex_pattern(self) -> Self: - """Verify that regex patterns are valid for MATCH operator.""" + """ + Validate that when the operator is MATCH, the rule is a valid regular expression string. + + Raises: + ValueError: If the operator is MATCH and `value` is + not a string or is not a valid regular expression. + + Returns: + Self: The same JwtRoleRule instance. + """ if self.operator == JsonPathOperator.MATCH: if not isinstance(self.value, str): raise ValueError( @@ -628,7 +739,14 @@ def check_regex_pattern(self) -> Self: @cached_property def compiled_regex(self) -> Optional[Pattern[str]]: - """Return compiled regex pattern for MATCH operator, None otherwise.""" + """ + Provide a compiled regex when the rule uses the MATCH operator and the value is a string. + + Returns: + Optional[Pattern[str]]: Compiled `re.Pattern` of `value` if + `operator` is `JsonPathOperator.MATCH` and `value` is a `str`, + `None` otherwise. + """ if self.operator == JsonPathOperator.MATCH and isinstance(self.value, str): return re.compile(self.value) return None @@ -817,7 +935,20 @@ class AuthenticationConfiguration(ConfigurationBase): @model_validator(mode="after") def check_authentication_model(self) -> Self: - """Validate YAML containing authentication configuration section.""" + """ + Validate authentication configuration and enforce module-specific requirements. + + Checks that the selected authentication module is supported and that any module-specific + configuration (JWK or RH Identity) is present when required. + + Returns: + self: The validated AuthenticationConfiguration instance. + + Raises: + ValueError: If the module is unsupported, or if a required module-specific + configuration (jwk_config for JWK token or rh_identity_config for RH Identity) + is missing. + """ if self.module not in constants.SUPPORTED_AUTHENTICATION_MODULES: supported_modules = ", ".join(constants.SUPPORTED_AUTHENTICATION_MODULES) raise ValueError( @@ -853,7 +984,16 @@ def check_authentication_model(self) -> Self: @property def jwk_configuration(self) -> JwkConfiguration: - """Return JWK configuration if the module is JWK token.""" + """ + Return the active JWK configuration for JWK-token authentication. + + Raises: + ValueError: If the authentication module is not the JWK token + module or if the JWK configuration is not set. + + Returns: + JwkConfiguration: The configured JWK settings. + """ if self.module != constants.AUTH_MOD_JWK_TOKEN: raise ValueError( "JWK configuration is only available for JWK token authentication module" @@ -864,7 +1004,16 @@ def jwk_configuration(self) -> JwkConfiguration: @property def rh_identity_configuration(self) -> RHIdentityConfiguration: - """Return RH Identity configuration if the module is RH Identity.""" + """ + Access the RH Identity configuration for RH Identity authentication. + + Returns: + RHIdentityConfiguration: The configured RH Identity configuration object. + + Raises: + ValueError: If the active authentication module is not RH Identity. + ValueError: If the RH Identity configuration is missing when the module is RH Identity. + """ if self.module != constants.AUTH_MOD_RH_IDENTITY: raise ValueError( "RH Identity configuration is only available for RH Identity authentication module" @@ -914,7 +1063,12 @@ def _validate_and_process(self) -> None: self.prompts = profile_module.PROFILE_CONFIG.get("system_prompts", {}) def get_prompts(self) -> dict[str, str]: - """Retrieve prompt attribute.""" + """ + Get the loaded prompt mappings for the custom profile. + + Returns: + dict[str, str]: Mapping from prompt names to prompt text. + """ return self.prompts @@ -929,7 +1083,17 @@ class Customization(ConfigurationBase): @model_validator(mode="after") def check_customization_model(self) -> Self: - """Load customizations.""" + """ + Load and apply service customization sources to the model. + + If `profile_path` is set, constructs a CustomProfile from that path and + assigns it to `custom_profile`. Otherwise, if `system_prompt_path` is + provided, validates the file and reads `system_prompt` from it. + + Returns: + self: The model instance, potentially with `custom_profile` or + `system_prompt` populated. + """ if self.profile_path: self.custom_profile = CustomProfile(path=self.profile_path) elif self.system_prompt_path is not None: @@ -957,7 +1121,15 @@ class InferenceConfiguration(ConfigurationBase): @model_validator(mode="after") def check_default_model_and_provider(self) -> Self: - """Check default model and provider.""" + """ + Validate that default_model and default_provider are configured together: both set or unset. + + Raises: + ValueError: If one is set while the other is not. + + Returns: + self (Self): The validated configuration instance. + """ if self.default_model is None and self.default_provider is not None: raise ValueError( "Default model must be specified when default provider is set" @@ -998,7 +1170,21 @@ class ConversationHistoryConfiguration(ConfigurationBase): @model_validator(mode="after") def check_cache_configuration(self) -> Self: - """Check conversation cache configuration.""" + """ + Validate the conversation cache configuration and enforce the selected cache type backend. + + Raises: + ValueError: If a backend is provided but `type` is None. + ValueError: If `type` is "memory" but `memory` config is missing, + or if other backend configs are present. + ValueError: If `type` is "sqlite" but `sqlite` config is missing, + or if other backend configs are present. + ValueError: If `type` is "postgres" but `postgres` config is + missing, or if other backend configs are present. + + Returns: + The validated model instance. + """ # if any backend config is provided, type must be explicitly selected if self.type is None: if any([self.memory, self.sqlite, self.postgres]): @@ -1279,6 +1465,15 @@ class Configuration(ConfigurationBase): ) def dump(self, filename: str = "configuration.json") -> None: - """Dump actual configuration into JSON file.""" + """ + Write the current Configuration model to a JSON file. + + The configuration is serialized with an indentation of 4 spaces using + the model's JSON representation and written with UTF-8 encoding. If the + file exists it will be overwritten. + + Parameters: + filename (str): Path to the output file (defaults to "configuration.json"). + """ with open(filename, "w", encoding="utf-8") as fout: fout.write(self.model_dump_json(indent=4)) From 4ef23609d5a4cf7cdc31c4d33715c3e90dcf760b Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 8 Dec 2025 09:24:33 +0100 Subject: [PATCH 2/3] Descriptions for request modes --- src/models/requests.py | 99 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/src/models/requests.py b/src/models/requests.py index c2e329b65..1a43a1737 100644 --- a/src/models/requests.py +++ b/src/models/requests.py @@ -197,13 +197,31 @@ class QueryRequest(BaseModel): @field_validator("conversation_id") @classmethod def check_uuid(cls, value: str | None) -> str | None: - """Check if conversation ID has the proper format.""" + """ + Validate that a conversation identifier matches the expected SUID format. + + Parameters: + value (str | None): Conversation identifier to validate; may be None. + + Returns: + str | None: The original `value` if valid or `None` if not provided. + + Raises: + ValueError: If `value` is provided and does not conform to the + expected SUID format. + """ if value and not suid.check_suid(value): raise ValueError(f"Improper conversation ID '{value}'") return value def get_documents(self) -> list[Document]: - """Return the list of documents from the attachments.""" + """ + Produce a list of Document objects derived from the model's attachments. + + Returns: + list[Document]: Documents created from attachments; empty list if + there are no attachments. + """ if not self.attachments: return [] return [ @@ -213,7 +231,15 @@ def get_documents(self) -> list[Document]: @model_validator(mode="after") def validate_provider_and_model(self) -> Self: - """Perform validation on the provider and model.""" + """ + Ensure `provider` and `model` are specified together. + + Raises: + ValueError: If only `provider` or only `model` is provided (they must be set together). + + Returns: + Self: The validated model instance. + """ if self.model and not self.provider: raise ValueError("Provider must be specified if model is specified") if self.provider and not self.model: @@ -222,7 +248,15 @@ def validate_provider_and_model(self) -> Self: @model_validator(mode="after") def validate_media_type(self) -> Self: - """Validate media_type field.""" + """ + Ensure the `media_type`, if present, is one of the allowed response media types. + + Raises: + ValueError: If `media_type` is not equal to `MEDIA_TYPE_JSON` or `MEDIA_TYPE_TEXT`. + + Returns: + Self: The model instance when validation passes. + """ if self.media_type and self.media_type not in [ MEDIA_TYPE_JSON, MEDIA_TYPE_TEXT, @@ -350,7 +384,18 @@ class FeedbackRequest(BaseModel): @field_validator("conversation_id") @classmethod def check_uuid(cls, value: str) -> str: - """Check if conversation ID has the proper format.""" + """ + Validate that a conversation identifier conforms to the application's SUID format. + + Parameters: + value (str): Conversation identifier to validate. + + Returns: + str: The validated conversation identifier. + + Raises: + ValueError: If `value` is not a valid SUID. + """ if not suid.check_suid(value): raise ValueError(f"Improper conversation ID {value}") return value @@ -358,7 +403,18 @@ def check_uuid(cls, value: str) -> str: @field_validator("sentiment") @classmethod def check_sentiment(cls, value: Optional[int]) -> Optional[int]: - """Check if sentiment value is as expected.""" + """ + Validate a sentiment value is one of the allowed options. + + Parameters: + value (Optional[int]): Sentiment value; must be -1, 1, or None. + + Returns: + Optional[int]: The validated sentiment value. + + Raises: + ValueError: If `value` is not -1, 1, or None. + """ if value not in {-1, 1, None}: raise ValueError( f"Improper sentiment value of {value}, needs to be -1 or 1" @@ -370,7 +426,19 @@ def check_sentiment(cls, value: Optional[int]) -> Optional[int]: def validate_categories( cls, value: Optional[list[FeedbackCategory]] ) -> Optional[list[FeedbackCategory]]: - """Validate feedback categories list.""" + """ + Normalize and deduplicate a feedback categories list. + + Converts an empty list to None for consistency and removes duplicate + categories while preserving their original order. If `value` is None, + it is returned unchanged. + + Parameters: + value (Optional[list[FeedbackCategory]]): List of feedback categories or None. + + Returns: + Optional[list[FeedbackCategory]]: The normalized list with duplicates removed, or None. + """ if value is None: return value @@ -382,7 +450,15 @@ def validate_categories( @model_validator(mode="after") def check_feedback_provided(self) -> Self: - """Ensure that at least one form of feedback is provided.""" + """ + Ensure at least one form of feedback is provided. + + Raises: + ValueError: If none of 'sentiment', 'user_feedback', or 'categories' are provided. + + Returns: + Self: The validated FeedbackRequest instance. + """ if ( self.sentiment is None and (self.user_feedback is None or self.user_feedback == "") @@ -419,7 +495,12 @@ class FeedbackStatusUpdateRequest(BaseModel): model_config = {"extra": "forbid"} def get_value(self) -> bool: - """Return the value of the status attribute.""" + """ + Get the desired feedback enablement status. + + Returns: + bool: `true` if feedback is enabled, `false` otherwise. + """ return self.status From 96a96d69045ba7dd42ce2571310ed99753119f67 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 8 Dec 2025 09:35:15 +0100 Subject: [PATCH 3/3] Descriptions for responses models --- src/models/responses.py | 269 ++++++++++++++++++++++++++++++++++------ 1 file changed, 228 insertions(+), 41 deletions(-) diff --git a/src/models/responses.py b/src/models/responses.py index e893d4e72..f59886a7f 100644 --- a/src/models/responses.py +++ b/src/models/responses.py @@ -744,11 +744,15 @@ class ConversationDeleteResponse(AbstractSuccessfulResponse): ) def __init__(self, *, deleted: bool, conversation_id: str) -> None: - """Initialize a ConversationDeleteResponse. + """ + Initialize a ConversationDeleteResponse and populate its public fields. + + If `deleted` is True the response message is "Conversation deleted + successfully"; otherwise it is "Conversation cannot be deleted". - Args: - deleted: Whether the conversation was successfully deleted. - conversation_id: The ID of the conversation that was deleted. + Parameters: + deleted (bool): Whether the conversation was successfully deleted. + conversation_id (str): The ID of the conversation. """ response_msg = ( "Conversation deleted successfully" @@ -786,7 +790,22 @@ def __init__(self, *, deleted: bool, conversation_id: str) -> None: @classmethod def openapi_response(cls) -> dict[str, Any]: - """Generate FastAPI response dict, using examples from model_config.""" + """ + Build an OpenAPI-compatible FastAPI response dict using the model's examples. + + Extracts labeled examples from the model's JSON schema `examples` and + places them under `application/json` -> `examples`. The returned + mapping includes a `description` ("Successful response"), the `model` + (the class itself), and `content` containing the assembled examples. + + Returns: + response (dict[str, Any]): A dict with keys `description`, `model`, + and `content` suitable for FastAPI/OpenAPI response registration. + + Raises: + SchemaError: If any example in the model's JSON schema is missing a + required `label` or `value`. + """ schema = cls.model_json_schema() model_examples = schema.get("examples", []) @@ -1097,12 +1116,13 @@ class AbstractErrorResponse(BaseModel): detail: DetailModel def __init__(self, *, response: str, cause: str, status_code: int): - """Initialize an AbstractErrorResponse. + """ + Create an error response model with an HTTP status code and detailed message. - Args: - response: Short summary of the error. - cause: Detailed explanation of what caused the error. - status_code: HTTP status code for the error response. + Parameters: + response (str): A short, user-facing summary of the error. + cause (str): A more detailed explanation of the error cause. + status_code (int): The HTTP status code to associate with this error response. """ super().__init__( status_code=status_code, detail=DetailModel(response=response, cause=cause) @@ -1110,12 +1130,40 @@ def __init__(self, *, response: str, cause: str, status_code: int): @classmethod def get_description(cls) -> str: - """Get the description from the class attribute or docstring.""" + """ + Retrieve the class description. + + Returns: + str: The class `description` attribute if present; otherwise the + class docstring; if neither is present, an empty string. + """ return getattr(cls, "description", cls.__doc__ or "") @classmethod def openapi_response(cls, examples: Optional[list[str]] = None) -> dict[str, Any]: - """Generate FastAPI response dict with examples from model_config.""" + """ + Build an OpenAPI/FastAPI response dictionary that exposes the model's labeled examples. + + Extracts examples from the model's JSON schema and includes them as + named application/json examples in the returned response mapping. If + the optional `examples` list is provided, only examples whose labels + appear in that list are included. Each included example is exposed + under its label with a `value` containing a `detail` payload. + + Parameters: + examples (Optional[list[str]]): If provided, restricts which + labeled examples to include by label. + + Returns: + dict[str, Any]: A response mapping with keys: + - "description": the response description, + - "model": the model class, + - "content": a mapping for "application/json" to the examples + object (or None if no examples). + + Raises: + SchemaError: If any example in the model schema lacks a `label`. + """ schema = cls.model_json_schema() model_examples = schema.get("examples", []) @@ -1162,11 +1210,12 @@ class BadRequestResponse(AbstractErrorResponse): } def __init__(self, *, resource: str, resource_id: str): - """Initialize a BadRequestResponse for invalid resource ID format. + """ + Create a 400 Bad Request response for an invalid resource ID format. - Args: - resource: The type of resource (e.g., "conversation", "provider"). - resource_id: The invalid resource ID. + Parameters: + resource (str): Type of the resource (for message), e.g., "conversation" or "provider". + resource_id (str): The invalid resource identifier used in the error message. """ response = f"Invalid {resource} ID format" cause = f"The {resource} ID {resource_id} has invalid format." @@ -1243,7 +1292,16 @@ class UnauthorizedResponse(AbstractErrorResponse): } def __init__(self, *, cause: str): - """Initialize UnauthorizedResponse.""" + """ + Create an UnauthorizedResponse describing missing or invalid client credentials. + + Initializes the error with a standardized response message and the + provided cause, and sets the HTTP status to 401 Unauthorized. + + Parameters: + cause (str): Human-readable explanation of why the request is + unauthorized (e.g. "missing token", "token expired"). + """ response_msg = "Missing or invalid credentials provided by client" super().__init__( response=response_msg, cause=cause, status_code=status.HTTP_401_UNAUTHORIZED @@ -1313,7 +1371,19 @@ class ForbiddenResponse(AbstractErrorResponse): def conversation( cls, action: str, resource_id: str, user_id: str ) -> "ForbiddenResponse": - """Create a ForbiddenResponse for conversation access denied.""" + """ + Create a ForbiddenResponse for a denied conversation action. + + Parameters: + action (str): The attempted action (e.g., "read", "delete", "update"). + resource_id (str): The conversation identifier targeted by the action. + user_id (str): The identifier of the user who attempted the action. + + Returns: + ForbiddenResponse: Error response indicating the user is not + permitted to perform the specified action on the conversation, with + `response` and `cause` fields populated. + """ response = "User does not have permission to perform this action" cause = ( f"User {user_id} does not have permission to " @@ -1323,14 +1393,30 @@ def conversation( @classmethod def endpoint(cls, user_id: str) -> "ForbiddenResponse": - """Create a ForbiddenResponse for endpoint access denied.""" + """ + Create a ForbiddenResponse indicating the specified user is denied access to the endpoint. + + Parameters: + user_id (str): Identifier of the user denied access. + + Returns: + ForbiddenResponse: Error response with a message and a cause + referencing the given `user_id`. + """ response = "User does not have permission to access this endpoint" cause = f"User {user_id} is not authorized to access this endpoint." return cls(response=response, cause=cause) @classmethod def feedback_disabled(cls) -> "ForbiddenResponse": - """Create a ForbiddenResponse for disabled feedback.""" + """ + Create a ForbiddenResponse indicating that storing feedback is disabled. + + Returns: + ForbiddenResponse: Error response with `response` set to "Storing + feedback is disabled" and `cause` set to "Storing feedback is + disabled." + """ return cls( response="Storing feedback is disabled", cause="Storing feedback is disabled.", @@ -1338,7 +1424,14 @@ def feedback_disabled(cls) -> "ForbiddenResponse": @classmethod def model_override(cls) -> "ForbiddenResponse": - """Create a ForbiddenResponse for model/provider override denied.""" + """ + Create a ForbiddenResponse indicating that overriding the model or provider is disallowed. + + Returns: + ForbiddenResponse: An error response with a user-facing message + instructing removal of model/provider fields and a cause stating + the missing `MODEL_OVERRIDE` permission. + """ return cls( response=( "This instance does not permit overriding model/provider in the " @@ -1352,7 +1445,14 @@ def model_override(cls) -> "ForbiddenResponse": ) def __init__(self, *, response: str, cause: str): - """Initialize a ForbiddenResponse.""" + """ + Construct a ForbiddenResponse with a public response message and an internal cause. + + Parameters: + response (str): Human-facing error message describing the forbidden action. + cause (str): Detailed cause or reason for the denial intended + for logs or diagnostics. + """ super().__init__( response=response, cause=cause, status_code=status.HTTP_403_FORBIDDEN ) @@ -1403,11 +1503,12 @@ class NotFoundResponse(AbstractErrorResponse): } def __init__(self, *, resource: str, resource_id: str): - """Initialize a NotFoundResponse for a missing resource. + """ + Create a NotFoundResponse for a missing resource and set the HTTP status to 404. - Args: - resource: The type of resource that was not found (e.g., "conversation", "model"). - resource_id: The ID of the resource that was not found. + Parameters: + resource (str): Resource type that was not found (e.g., "conversation", "model"). + resource_id (str): Identifier of the missing resource. """ response = f"{resource.title()} not found" cause = f"{resource.title()} with ID {resource_id} does not exist" @@ -1450,7 +1551,13 @@ class UnprocessableEntityResponse(AbstractErrorResponse): } def __init__(self, *, response: str, cause: str): - """Initialize UnprocessableEntityResponse.""" + """ + Create a 422 Unprocessable Entity error response. + + Parameters: + response (str): Human-readable error message describing what was unprocessable. + cause (str): Specific cause or diagnostic information explaining the error. + """ super().__init__( response=response, cause=cause, @@ -1520,20 +1627,49 @@ class QuotaExceededResponse(AbstractErrorResponse): @classmethod def model(cls, model_name: str) -> "QuotaExceededResponse": - """Create a QuotaExceededResponse for model quota exceeded.""" + """ + Create a QuotaExceededResponse for a specific model. + + Parameters: + model_name (str): The model identifier whose token quota was exceeded. + + Returns: + QuotaExceededResponse: Response with a standard response message + and a cause that includes the model name. + """ response = "The model quota has been exceeded" cause = f"The token quota for model {model_name} has been exceeded." return cls(response=response, cause=cause) @classmethod def from_exception(cls, exc: QuotaExceedError) -> "QuotaExceededResponse": - """Create a QuotaExceededResponse from a QuotaExceedError exception.""" + """ + Construct a QuotaExceededResponse representing the provided QuotaExceedError. + + Parameters: + exc: The QuotaExceedError instance whose message will be used as + the cause. + + Returns: + QuotaExceededResponse initialized with a standard quota-exceeded + message and the exception's text as the cause. + """ response = "The quota has been exceeded" cause = str(exc) return cls(response=response, cause=cause) def __init__(self, *, response: str, cause: str) -> None: - """Initialize a QuotaExceededResponse.""" + """ + Create a QuotaExceededResponse with a public message and an explanatory cause. + + Parameters: + response (str): Public-facing error message describing the quota condition. + cause (str): Detailed cause or internal explanation for the quota + exceedance; stored in the error detail. + + Notes: + Sets the response's HTTP status code to 429 (Too Many Requests). + """ super().__init__( response=response, cause=cause, @@ -1597,7 +1733,13 @@ class InternalServerErrorResponse(AbstractErrorResponse): @classmethod def generic(cls) -> "InternalServerErrorResponse": - """Create a generic InternalServerErrorResponse.""" + """ + Create an InternalServerErrorResponse representing a generic internal server error. + + @returns InternalServerErrorResponse: instance with a standard response + message ("Internal server error") and a cause explaining an unexpected + processing error. + """ return cls( response="Internal server error", cause="An unexpected error occurred while processing the request.", @@ -1605,7 +1747,13 @@ def generic(cls) -> "InternalServerErrorResponse": @classmethod def configuration_not_loaded(cls) -> "InternalServerErrorResponse": - """Create an InternalServerErrorResponse for configuration not loaded.""" + """ + Create an InternalServerErrorResponse indicating the service config was not initialized. + + @returns InternalServerErrorResponse with response "Configuration is + not loaded" and cause "Lightspeed Stack configuration has not been + initialized." + """ return cls( response="Configuration is not loaded", cause="Lightspeed Stack configuration has not been initialized.", @@ -1613,7 +1761,17 @@ def configuration_not_loaded(cls) -> "InternalServerErrorResponse": @classmethod def feedback_path_invalid(cls, path: str) -> "InternalServerErrorResponse": - """Create an InternalServerErrorResponse for invalid feedback path.""" + """ + Create an InternalServerErrorResponse describing a failure to store feedback. + + Parameters: + path (str): Filesystem directory where feedback storage was attempted. + + Returns: + InternalServerErrorResponse: Error response with a response message + "Failed to store feedback" and a cause indicating the failed + directory. + """ return cls( response="Failed to store feedback", cause=f"Failed to store feedback at directory: {path}", @@ -1621,7 +1779,17 @@ def feedback_path_invalid(cls, path: str) -> "InternalServerErrorResponse": @classmethod def query_failed(cls, backend_url: str) -> "InternalServerErrorResponse": - """Create an InternalServerErrorResponse for query failure.""" + """ + Create an InternalServerErrorResponse representing a failed query to an external backend. + + Parameters: + backend_url (str): The backend URL included in the error cause message. + + Returns: + InternalServerErrorResponse: An error response with response "Error + while processing query" and cause "Failed to call backend: + {backend_url}". + """ return cls( response="Error while processing query", cause=f"Failed to call backend: {backend_url}", @@ -1629,7 +1797,13 @@ def query_failed(cls, backend_url: str) -> "InternalServerErrorResponse": @classmethod def cache_unavailable(cls) -> "InternalServerErrorResponse": - """Create an InternalServerErrorResponse for cache unavailable.""" + """ + Create an InternalServerErrorResponse indicating the conversation cache is unavailable. + + Returns: + InternalServerErrorResponse: Error response with a message that the + conversation cache is not configured and a corresponding cause. + """ return cls( response="Conversation cache not configured", cause="Conversation cache is not configured or unavailable.", @@ -1637,14 +1811,26 @@ def cache_unavailable(cls) -> "InternalServerErrorResponse": @classmethod def database_error(cls) -> "InternalServerErrorResponse": - """Create an InternalServerErrorResponse for database error.""" + """ + Create an InternalServerErrorResponse representing a database query failure. + + Returns: + InternalServerErrorResponse: Instance with response "Database query + failed" and cause "Failed to query the database". + """ return cls( response="Database query failed", cause="Failed to query the database", ) def __init__(self, *, response: str, cause: str) -> None: - """Initialize an InternalServerErrorResponse.""" + """ + Initialize the error response for internal server errors and set the HTTP status code. + + Parameters: + response (str): Public-facing error message. + cause (str): Internal explanation of the error cause. + """ super().__init__( response=response, cause=cause, @@ -1671,11 +1857,12 @@ class ServiceUnavailableResponse(AbstractErrorResponse): } def __init__(self, *, backend_name: str, cause: str): - """Initialize a ServiceUnavailableResponse. + """ + Construct a ServiceUnavailableResponse indicating the specified backend cannot be reached. - Args: - backend_name: The name of the backend service that is unavailable. - cause: Detailed explanation of why the service is unavailable. + Parameters: + backend_name (str): Name of the backend service that could not be contacted. + cause (str): Detailed explanation of why the service is unavailable. """ response = f"Unable to connect to {backend_name}" super().__init__(