diff --git a/pyproject.toml b/pyproject.toml index a93eb2a50..dbe47a8ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -257,7 +257,7 @@ line-length = 88 extend-exclude = ["tests/profiles/syntax_error.py"] [tool.ruff.lint] -extend-select = ["C", "FA", "FLY", "I", "ISC", "TID251", "UP006", "UP007", "UP008", "UP010", "UP012", "UP017", "UP024", "UP035", "UP040", "UP041", "RUF100", "B009", "B010", "B011", "BLE", "DTZ005", "D202", "PLR1733", "RUF022", "PLW1510", "TC004", "PIE790", "PERF402", "FURB129", "RET501", "LOG"] +extend-select = ["C", "FA", "FLY", "I", "ISC", "TID251", "UP006", "UP007", "UP008", "UP010", "UP012", "UP017", "UP024", "UP035", "UP040", "UP041", "RUF100", "B009", "B010", "B011", "BLE", "DTZ005", "D202", "PLR1733", "RUF022", "PLW1510", "TC004", "PIE790", "PERF402", "FURB129", "RET501", "LOG", "D413"] ignore = ["UP047", "UP045", "S", "C901", "RUF", "SIM", "B017", "TRY004", "TRY201", "TRY203", "TRY401", "B008", "EXE001", "G201", "PYI034", "PYI064"] [tool.ruff.lint.flake8-tidy-imports.banned-api] diff --git a/src/a2a_storage/context_store.py b/src/a2a_storage/context_store.py index 80a8539da..a002f4b88 100644 --- a/src/a2a_storage/context_store.py +++ b/src/a2a_storage/context_store.py @@ -23,6 +23,7 @@ async def get(self, context_id: str) -> Optional[str]: Returns: The OGX conversation ID, or None if not found. + """ @abstractmethod @@ -32,6 +33,7 @@ async def set(self, context_id: str, conversation_id: str) -> None: Args: context_id: The A2A context ID. conversation_id: The OGX conversation ID. + """ @abstractmethod @@ -40,6 +42,7 @@ async def delete(self, context_id: str) -> None: Args: context_id: The A2A context ID to delete. + """ @abstractmethod @@ -55,4 +58,5 @@ def ready(self) -> bool: Returns: True if the store is initialized and ready, False otherwise. + """ diff --git a/src/a2a_storage/in_memory_context_store.py b/src/a2a_storage/in_memory_context_store.py index 7ad02135e..df364ce7c 100644 --- a/src/a2a_storage/in_memory_context_store.py +++ b/src/a2a_storage/in_memory_context_store.py @@ -35,6 +35,7 @@ async def get(self, context_id: str) -> Optional[str]: Returns: The OGX conversation ID, or None if not found. + """ async with self._lock: conversation_id = self._contexts.get(context_id) @@ -52,6 +53,7 @@ async def set(self, context_id: str, conversation_id: str) -> None: Args: context_id: The A2A context ID. conversation_id: The OGX conversation ID. + """ async with self._lock: self._contexts[context_id] = conversation_id @@ -66,6 +68,7 @@ async def delete(self, context_id: str) -> None: Args: context_id: The A2A context ID to delete. + """ async with self._lock: if context_id in self._contexts: @@ -89,5 +92,6 @@ def ready(self) -> bool: Returns: True, as in-memory store is always ready after construction. + """ return self._initialized diff --git a/src/a2a_storage/postgres_context_store.py b/src/a2a_storage/postgres_context_store.py index 99dbf477e..44c88c742 100644 --- a/src/a2a_storage/postgres_context_store.py +++ b/src/a2a_storage/postgres_context_store.py @@ -43,6 +43,7 @@ def __init__( Args: engine: SQLAlchemy async engine connected to the PostgreSQL database. create_table: If True, create the table on initialization. + """ logger.debug("Initializing PostgresA2AContextStore") self._engine = engine @@ -75,6 +76,7 @@ async def get(self, context_id: str) -> Optional[str]: Returns: The OGX conversation ID, or None if not found. + """ await self._ensure_initialized() @@ -99,6 +101,7 @@ async def set(self, context_id: str, conversation_id: str) -> None: Args: context_id: The A2A context ID. conversation_id: The OGX conversation ID. + """ await self._ensure_initialized() @@ -124,6 +127,7 @@ async def delete(self, context_id: str) -> None: Args: context_id: The A2A context ID to delete. + """ await self._ensure_initialized() @@ -139,5 +143,6 @@ def ready(self) -> bool: Returns: True if the store is initialized, False otherwise. + """ return self._initialized diff --git a/src/a2a_storage/sqlite_context_store.py b/src/a2a_storage/sqlite_context_store.py index bada818e3..dde7c500c 100644 --- a/src/a2a_storage/sqlite_context_store.py +++ b/src/a2a_storage/sqlite_context_store.py @@ -42,6 +42,7 @@ def __init__( Args: engine: SQLAlchemy async engine connected to the SQLite database. create_table: If True, create the table on initialization. + """ logger.debug("Initializing SQLiteA2AContextStore") self._engine = engine @@ -74,6 +75,7 @@ async def get(self, context_id: str) -> Optional[str]: Returns: The OGX conversation ID, or None if not found. + """ await self._ensure_initialized() @@ -98,6 +100,7 @@ async def set(self, context_id: str, conversation_id: str) -> None: Args: context_id: The A2A context ID. conversation_id: The OGX conversation ID. + """ await self._ensure_initialized() @@ -125,6 +128,7 @@ async def delete(self, context_id: str) -> None: Args: context_id: The A2A context ID to delete. + """ await self._ensure_initialized() @@ -140,5 +144,6 @@ def ready(self) -> bool: Returns: True if the store is initialized, False otherwise. + """ return self._initialized diff --git a/src/a2a_storage/storage_factory.py b/src/a2a_storage/storage_factory.py index 9098fa7fd..067fee1af 100644 --- a/src/a2a_storage/storage_factory.py +++ b/src/a2a_storage/storage_factory.py @@ -38,6 +38,7 @@ async def create_task_store(cls, config: A2AStateConfiguration) -> TaskStore: Returns: TaskStore implementation (InMemoryTaskStore or DatabaseTaskStore). + """ if cls._task_store is not None: return cls._task_store @@ -86,6 +87,7 @@ async def create_context_store( Returns: A2AContextStore implementation. + """ if cls._context_store is not None: return cls._context_store @@ -131,6 +133,7 @@ async def _get_or_create_engine(cls, config: A2AStateConfiguration) -> AsyncEngi Returns: SQLAlchemy AsyncEngine. + """ if cls._engine is not None: return cls._engine diff --git a/src/app/database.py b/src/app/database.py index 0b6881896..4b1b0bae9 100644 --- a/src/app/database.py +++ b/src/app/database.py @@ -29,6 +29,7 @@ def get_engine() -> Engine: Raises: RuntimeError: If the database engine has not been initialized; call initialize_database() first. + """ if engine is None: raise RuntimeError( @@ -45,6 +46,7 @@ def create_tables() -> None: Raises: RuntimeError: If the global database engine is not initialized (call initialize_database() first). + """ Base.metadata.create_all(get_engine()) @@ -60,6 +62,7 @@ def get_session() -> Session: Raises: RuntimeError: If the database has not been initialized; call initialize_database() first. + """ if session_local is None: raise RuntimeError( @@ -85,6 +88,7 @@ def _create_sqlite_engine(config: SQLiteDatabaseConfiguration, **kwargs: Any) -> ------ FileNotFoundError: If the parent directory of `config.db_path` does not exist. RuntimeError: If engine creation fails. + """ if not Path(config.db_path).parent.exists(): raise FileNotFoundError( @@ -122,6 +126,7 @@ def _create_postgres_engine( Raises: ------ RuntimeError: If engine creation fails or if creating the specified schema fails. + """ postgres_url = ( f"postgresql://{config.user}:{config.password.get_secret_value()}@" diff --git a/src/app/endpoints/a2a.py b/src/app/endpoints/a2a.py index 3f1ed2ad1..7505afc54 100644 --- a/src/app/endpoints/a2a.py +++ b/src/app/endpoints/a2a.py @@ -104,6 +104,7 @@ async def _get_task_store() -> TaskStore: Returns: TaskStore instance based on configuration. + """ global _TASK_STORE # pylint: disable=global-statement if _TASK_STORE is None: @@ -116,6 +117,7 @@ async def _get_context_store() -> A2AContextStore: Returns: A2AContextStore instance based on configuration. + """ global _CONTEXT_STORE # pylint: disable=global-statement if _CONTEXT_STORE is None: @@ -141,6 +143,7 @@ def _build_a2a_parts_from_agent_result( Returns: List of A2A Part objects for the final artifact. + """ if run_result is not None: final_text = run_result.response.text or "".join(accumulated_text) @@ -157,6 +160,7 @@ def _record_model_span(span: trace.Span, model_id: str) -> None: Parameters: span: The active OpenTelemetry span. model_id: Full model identifier in "provider/model" format. + """ provider_id, bare_model_id = extract_provider_and_model_from_model_id(model_id) set_span_attributes( @@ -183,6 +187,7 @@ def _record_execution_span( run_result: Completed agent run result, or None. compacted: Whether the turn used compacted conversation context. inference_time: Request processing duration in seconds. + """ if tool_call_names: set_span_attributes( @@ -234,6 +239,7 @@ async def _persist_compacted_a2a_turn( the request was served in compacted mode. agent: The pydantic-ai agent whose model captured the output items. task_id: A2A task identifier, used for error reporting. + """ if not compaction.compacted or compaction.original_input is None: return @@ -275,6 +281,7 @@ def process_event( Args: event: The event to process + """ if isinstance(event, TaskStatusUpdateEvent): if event.status.state == TaskState.failed: @@ -335,6 +342,7 @@ def __init__( auth_token: Authentication token for the request mcp_headers: MCP headers for context propagation request_headers: Incoming HTTP request headers for allowlist propagation + """ self.auth_token: str = auth_token self.mcp_headers: McpHeaders = mcp_headers or {} @@ -352,6 +360,7 @@ async def execute( Args: context: The request context containing user input and metadata event_queue: Queue for sending response events + """ if not context.message: raise ValueError("A2A request must have a message") @@ -406,6 +415,7 @@ async def _process_task_streaming( # pylint: disable=too-many-locals,too-many-s task_updater: Task updater for sending events task_id: The task ID to use for this execution context_id: The context ID to use for this execution + """ if not task_id or not context_id: raise ValueError("Task ID and Context ID are required") @@ -628,6 +638,7 @@ async def _convert_stream_to_events( Yields: A2A events (TaskStatusUpdateEvent or TaskArtifactUpdateEvent) + """ if not task_id or not context_id: raise ValueError("Task ID and Context ID are required") @@ -686,6 +697,7 @@ def _dispatch_agent_event( # pylint: disable=too-many-arguments,too-many-positi Returns: A2A TaskStatusUpdateEvent, or None if the event is not mapped. + """ _ = artifact_id @@ -752,6 +764,7 @@ def _text_status_event( Returns: TaskStatusUpdateEvent with the text delta. + """ return TaskStatusUpdateEvent( task_id=task_id, @@ -781,6 +794,7 @@ async def cancel( Raises: NotImplementedError: Task cancellation is not currently supported + """ logger.info("Cancellation requested but not currently supported") raise NotImplementedError("Task cancellation not currently supported") @@ -798,6 +812,7 @@ def get_lightspeed_agent_card() -> AgentCard: Returns: AgentCard: The agent card describing Lightspeed's capabilities. + """ # Get base URL from configuration or construct it service_config = configuration.service_configuration @@ -922,6 +937,7 @@ async def _create_a2a_app( Returns: A2A Starlette ASGI application + """ agent_executor = A2AAgentExecutor( auth_token=auth_token, @@ -1061,6 +1077,7 @@ async def _handle_a2a_jsonrpc( # pylint: disable=too-many-locals,too-many-state Returns: JSON-RPC response or streaming response + """ logger.debug("A2A endpoint called: %s %s", request.method, request.url.path) diff --git a/src/app/endpoints/conversations_v1.py b/src/app/endpoints/conversations_v1.py index d326ca0c1..324826de8 100644 --- a/src/app/endpoints/conversations_v1.py +++ b/src/app/endpoints/conversations_v1.py @@ -234,6 +234,7 @@ async def get_conversation_endpoint_handler( # pylint: disable=too-many-locals, Returns: ConversationResponse: Structured response containing the conversation ID and simplified chat history + """ with tracer.start_as_current_span("conversations_v1.get") as span: check_configuration_loaded(configuration) @@ -356,6 +357,7 @@ async def delete_conversation_endpoint_handler( Returns: ConversationDeleteResponse: Response indicating the result of the deletion operation + """ with tracer.start_as_current_span("conversations_v1.delete") as span: check_configuration_loaded(configuration) @@ -477,6 +479,7 @@ async def update_conversation_endpoint_handler( # pylint: disable=too-many-stat Returns: ConversationUpdateResponse: Response indicating the result of the update operation + """ with tracer.start_as_current_span("conversations_v1.update") as span: check_configuration_loaded(configuration) diff --git a/src/app/endpoints/conversations_v2.py b/src/app/endpoints/conversations_v2.py index 0478caa6d..e95e61efd 100644 --- a/src/app/endpoints/conversations_v2.py +++ b/src/app/endpoints/conversations_v2.py @@ -275,6 +275,7 @@ def build_conversation_turn_from_cache_entry(entry: CacheEntry) -> ConversationT Returns: ConversationTurn object with messages, tool_calls, tool_results, and timestamps + """ # Create Message objects for user and assistant messages = [ diff --git a/src/app/endpoints/feedback.py b/src/app/endpoints/feedback.py index 29e4a979d..854093ea2 100644 --- a/src/app/endpoints/feedback.py +++ b/src/app/endpoints/feedback.py @@ -79,6 +79,7 @@ def is_feedback_enabled() -> bool: Returns: bool: True if feedback collection is enabled; otherwise, False. + """ return configuration.user_data_collection_configuration.feedback_enabled @@ -94,6 +95,7 @@ async def assert_feedback_enabled(_request: Request) -> None: Raises: HTTPException: If feedback collection is disabled. + """ feedback_enabled = is_feedback_enabled() if not feedback_enabled: @@ -115,6 +117,7 @@ def _record_feedback_request_attributes( span: The root feedback span to annotate. feedback_request: The incoming feedback request. user_id: The authenticated user identifier (anonymized before recording). + """ set_span_attributes( span, @@ -241,6 +244,7 @@ def store_feedback(user_id: str, feedback: dict) -> None: Raises: ------ HTTPException: If writing the feedback file fails (HTTP 500). + """ logger.debug("Storing feedback for user %s", user_id) # Creates storage path only if it doesn't exist. The `exist_ok=True` prevents diff --git a/src/app/endpoints/health.py b/src/app/endpoints/health.py index 7e0becbd0..d7b18081b 100644 --- a/src/app/endpoints/health.py +++ b/src/app/endpoints/health.py @@ -65,6 +65,7 @@ async def get_providers_health_statuses() -> list[ProviderHealthStatus]: list[ProviderHealthStatus]: A list containing the health status of each provider. If provider health cannot be determined, returns a single entry indicating an error. + """ try: client = AsyncOgxClientHolder().get_client() @@ -102,6 +103,7 @@ async def check_default_model_available() -> tuple[bool, str]: A tuple of (available, reason) where available is True if the default model was found or no default model is configured, and reason describes the outcome. + """ inference = configuration.inference if ( diff --git a/src/app/endpoints/providers.py b/src/app/endpoints/providers.py index 898c10cab..b003799bf 100644 --- a/src/app/endpoints/providers.py +++ b/src/app/endpoints/providers.py @@ -117,6 +117,7 @@ def group_providers( Returns: Mapping from API type to list of providers containing only 'provider_id' and 'provider_type'. + """ result: dict[str, list[dict[str, Any]]] = {} for provider in providers: diff --git a/src/app/endpoints/query.py b/src/app/endpoints/query.py index 0e3a7fb4c..6322c3e16 100644 --- a/src/app/endpoints/query.py +++ b/src/app/endpoints/query.py @@ -150,6 +150,7 @@ async def _handle_query_with_tracing( Raises: HTTPException: On authentication, authorization, quota, or model errors. + """ check_configuration_loaded(configuration) diff --git a/src/app/endpoints/rags.py b/src/app/endpoints/rags.py index 106a70701..a4af76a11 100644 --- a/src/app/endpoints/rags.py +++ b/src/app/endpoints/rags.py @@ -133,6 +133,7 @@ def _resolve_rag_id_to_vector_db_id(rag_id: str, byok_rags: list[RagStore]) -> s Returns: ------- The OGX vector_db_id, or the original ID if no mapping found. + """ for brag in byok_rags: if brag.rag_id == rag_id: diff --git a/src/app/endpoints/responses.py b/src/app/endpoints/responses.py index df12cbfe4..928b21651 100644 --- a/src/app/endpoints/responses.py +++ b/src/app/endpoints/responses.py @@ -140,6 +140,7 @@ def _count_request_attachments(response_input: ResponseInput) -> int: Returns: Number of input_file and input_image content parts. + """ if isinstance(response_input, str): return 0 @@ -170,6 +171,7 @@ def _finalize_responses_root_span( turn_summary: Completed turn summary with the LLM response text. compacted: Whether the turn used compacted conversation context. session_id: Normalized conversation id for ``session.id``. + """ set_span_attributes( root_span, @@ -190,6 +192,7 @@ def _start_llm_inference_span( Returns: Started OpenTelemetry span for the inference call. + """ provider_id, bare_model_id = extract_provider_and_model_from_model_id(model_id) span = tracer.start_span( @@ -220,6 +223,7 @@ def _complete_llm_inference_span( turn_summary: Completed turn summary with tools, tool-based RAG, and tokens. model: Composite model identifier in ``provider/model`` format. inference_time: Inference duration in seconds. + """ provider_id, bare_model_id = extract_provider_and_model_from_model_id(model) set_span_attributes( @@ -254,6 +258,7 @@ def _get_user_agent(request: Request) -> Optional[str]: Returns: Sanitized User-Agent string, or None if the header is absent or empty. + """ raw = request.headers.get("User-Agent", "") if not raw: @@ -299,6 +304,7 @@ def _error_response_for_response_api_error( Returns: Structured error response for known API failures, or None for unknown errors. + """ if isinstance(error, RuntimeError): if not is_context_length_error(str(error)): @@ -324,6 +330,7 @@ def _record_inference_span_exception( inference_span: The ``llm.inference`` span to annotate. error: Exception to record on the span. error_response: Mapped structured error response for attribute enrichment. + """ attributes = ( { @@ -353,6 +360,7 @@ def _raise_response_api_http_exception( Raises: Exception: Re-raises unknown RuntimeError instances unchanged. HTTPException: Raised for known Responses API failures. + """ error_response = _error_response_for_response_api_error(error, api_params) _record_inference_span_exception(inference_span, error, error_response) @@ -372,6 +380,7 @@ async def _persist_blocked_response_turn( Args: api_params: Responses API parameters for the blocked request. context: Request-scoped Responses API context with moderation details. + """ if api_params.store: moderation_result = cast("ShieldModerationBlocked", context.moderation_result) @@ -409,6 +418,7 @@ async def _append_previous_response_turn( api_params: Responses API parameters containing conversation details. context: Request-scoped Responses API context. output: Final output items from the Responses API object. + """ if not api_params.store: return @@ -446,6 +456,7 @@ def _store_response_query_results( Returns: True when query results were stored, False when storage is disabled. + """ if not api_params.store: return False @@ -502,6 +513,7 @@ async def responses_endpoint_handler( - 429: Quota limit exceeded - The token quota for model or user has been exceeded - 500: Internal Server Error - Configuration not loaded or other server errors - 503: Service Unavailable - Unable to connect to OGX backend + """ span_name = "responses.handle_request" if responses_request.stream: @@ -553,6 +565,7 @@ async def handle_responses_with_tracing( # pylint: disable=too-many-locals Returns: ResponsesResponse or StreamingResponse depending on ``stream``. + """ original_request = responses_request # read-only request updated_request = responses_request.model_copy(deep=True) @@ -761,6 +774,7 @@ def _record_response_inference_result( result: Result label such as ``success`` or ``failure``. duration: Inference call duration in seconds. record_failure: When True, also increment the LLM failure counter. + """ provider, model = extract_provider_and_model_from_model_id(model_id) if record_failure: @@ -783,6 +797,7 @@ async def handle_streaming_response( context: Responses context Returns: StreamingResponse with SSE-formatted events + """ root_span = context.root_span turn_summary = TurnSummary() @@ -852,6 +867,7 @@ async def shield_violation_generator( context: ResponsesContext Yields: SSE-formatted strings for streaming events + """ normalized_conv_id = normalize_conversation_id(api_params.conversation) available_quotas = get_available_quotas( @@ -944,6 +960,7 @@ def _sanitize_response_dict( configured_mcp_labels: Set of ``server_label`` values that identify server-deployed MCP servers. original_request: Original request object + """ if original_request.instructions is None: response_dict["instructions"] = SUBSTITUTED_INSTRUCTIONS_PLACEHOLDER @@ -980,6 +997,7 @@ def _is_server_mcp_output_item( Returns: True if the item is an MCP call/list/approval from a server-deployed MCP server. + """ item_type = item.get("type") if item_type in ("mcp_call", "mcp_list_tools", "mcp_approval_request"): @@ -1002,6 +1020,7 @@ def _should_filter_mcp_chunk( Returns: True if the chunk should be filtered out from the client stream. + """ if chunk.type == "response.output_item.added": item_added_chunk = cast("OutputItemAddedChunk", chunk) @@ -1047,6 +1066,7 @@ def _populate_turn_summary( api_params: ResponsesApiParams context: Responses context turn_summary: TurnSummary to populate + """ turn_summary.id = response_object.id vector_store_ids = extract_vector_store_ids_from_tools(api_params.tools) @@ -1094,6 +1114,7 @@ async def response_generator( inference_span: OpenTelemetry ``llm.inference`` span for this stream. Yields: SSE-formatted strings for streaming events, ending with [DONE] + """ logger.debug("Starting streaming response (Responses API) processing") @@ -1269,6 +1290,7 @@ async def generate_response( turn_summary: TurnSummary to populate during streaming Yields: SSE-formatted strings from the generator + """ root_span = context.root_span try: @@ -1325,6 +1347,7 @@ async def handle_non_streaming_response( context: Responses context Returns: ResponsesResponse with the completed response + """ root_span = context.root_span user_id = context.auth[0] diff --git a/src/app/endpoints/rlsapi_v1.py b/src/app/endpoints/rlsapi_v1.py index 123420a13..f952b5ba8 100644 --- a/src/app/endpoints/rlsapi_v1.py +++ b/src/app/endpoints/rlsapi_v1.py @@ -124,6 +124,7 @@ def _build_instructions(systeminfo: RlsapiV1SystemInfo) -> str: Returns: The rendered instructions string for the LLM. + """ prompt = ( configuration.customization.system_prompt @@ -156,6 +157,7 @@ def _compile_prompt_template(prompt: str) -> jinja2.Template: Raises: TemplateRenderError: If the template contains invalid Jinja2 syntax. + """ env = SandboxedEnvironment() try: @@ -178,6 +180,7 @@ async def _get_default_model_id() -> str: Raises: HTTPException: If no model can be determined from configuration or discovery. + """ # 1. Try configured defaults if configuration.inference is not None: @@ -237,6 +240,7 @@ async def _resolve_validated_model_id() -> str: Raises: HTTPException: 404 if the resolved model does not exist in OGX. HTTPException: 503 if OGX is unreachable during resolution or validation. + """ model_id = await _get_default_model_id() client = AsyncOgxClientHolder().get_client() @@ -272,6 +276,7 @@ async def _call_llm( Raises: ApiException: If the OGX service is unreachable. HTTPException: 503 if no default model is configured. + """ client = AsyncOgxClientHolder().get_client() resolved_model_id = model_id or await _get_default_model_id() @@ -324,6 +329,7 @@ def _queue_splunk_event( # pylint: disable=too-many-arguments,too-many-position sourcetype: Splunk sourcetype to use when sending the event. input_tokens: Number of prompt tokens consumed by the LLM call. output_tokens: Number of completion tokens produced by the LLM call. + """ org_id, system_id = get_rh_identity_context(request) systeminfo = infer_request.context.systeminfo @@ -383,6 +389,7 @@ async def _check_shield_moderation( # pylint: disable=too-many-arguments,too-ma A tuple of (refusal_response, moderated_input). refusal_response is None when moderation passed; moderated_input is the (possibly redacted) text to forward to inference. + """ logger.info("Running shield moderation for rlsapi v1 request %s", request_id) @@ -465,6 +472,7 @@ def _record_inference_failure( # pylint: disable=too-many-arguments,too-many-po Returns: The total inference time in seconds. + """ inference_time = time.monotonic() - start_time recording.record_llm_failure(provider, model, endpoint_path) @@ -504,6 +512,7 @@ def _resolve_quota_subject(request: Request, auth: AuthTuple) -> Optional[str]: Returns: The resolved subject identifier string, or None if quota is disabled. + """ quota_subject = configuration.rlsapi_v1.quota_subject if quota_subject is None: @@ -557,6 +566,7 @@ def _build_infer_response( Returns: Tuple of the assembled ``RlsapiV1InferResponse`` and its ``TurnSummary``. + """ if response is not None: turn_summary = build_turn_summary( @@ -690,6 +700,7 @@ async def infer_endpoint( # pylint: disable=R0914,R0915 Raises: HTTPException: 503 if the LLM service is unavailable. + """ # Authentication enforced by get_auth_dependency(), authorization by @authorize decorator. with tracer.start_as_current_span("rlsapi_v1.infer") as span: diff --git a/src/app/endpoints/streaming_query.py b/src/app/endpoints/streaming_query.py index 28cbd04e7..9ae82a6cb 100644 --- a/src/app/endpoints/streaming_query.py +++ b/src/app/endpoints/streaming_query.py @@ -194,6 +194,7 @@ async def _handle_streaming_query_with_tracing( # pylint: disable=too-many-loca Raises: HTTPException: On authentication, authorization, quota, or model errors. + """ check_configuration_loaded(configuration) @@ -410,6 +411,7 @@ async def generate_response_with_compaction( Yields: SSE-formatted strings. + """ try: media_type = context.query_request.media_type or MEDIA_TYPE_JSON diff --git a/src/app/endpoints/vector_stores.py b/src/app/endpoints/vector_stores.py index 65e8e2714..7e8477275 100644 --- a/src/app/endpoints/vector_stores.py +++ b/src/app/endpoints/vector_stores.py @@ -5,6 +5,8 @@ RAG configuration instead (see docs/user_doc/byok_guide.md). """ +# pylint: disable=too-many-lines + VECTOR_STORES_DEPRECATED_REASON: str = ( "OGX proxy API; deprecated and scheduled for removal in the next LCS release." ) @@ -187,6 +189,7 @@ async def create_vector_store( - 403: Authorization failed - 500: Lightspeed Stack configuration not loaded - 503: Unable to connect to OGX + """ _ = auth _ = request @@ -264,6 +267,7 @@ async def list_vector_stores( - 403: Authorization failed - 500: Lightspeed Stack configuration not loaded - 503: Unable to connect to OGX + """ _ = auth _ = request @@ -329,6 +333,7 @@ async def get_vector_store( - 404: Vector store not found - 500: Lightspeed Stack configuration not loaded - 503: Unable to connect to OGX + """ _ = auth _ = request @@ -397,6 +402,7 @@ async def update_vector_store( - 404: Vector store not found - 500: Lightspeed Stack configuration not loaded - 503: Unable to connect to OGX + """ _ = auth _ = request @@ -467,6 +473,7 @@ async def delete_vector_store( Returns: VectorStoreDeleteResponse: Delete outcome for the requested vector store. + """ _ = auth _ = request @@ -521,6 +528,7 @@ async def create_file( # pylint: disable=too-many-branches,too-many-statements - 429: Too many concurrent file uploads - 500: Lightspeed Stack configuration not loaded - 503: Unable to connect to OGX + """ _ = auth @@ -664,6 +672,7 @@ async def add_file_to_vector_store( # pylint: disable=too-many-locals,too-many- - 429: Too many concurrent vector store file attachments - 500: Lightspeed Stack configuration not loaded - 503: Unable to connect to OGX + """ _ = auth _ = request @@ -820,6 +829,7 @@ async def list_vector_store_files( - 404: Vector store not found - 500: Lightspeed Stack configuration not loaded - 503: Unable to connect to OGX + """ _ = auth _ = request @@ -893,6 +903,7 @@ async def get_vector_store_file( - 404: File not found in vector store - 500: Lightspeed Stack configuration not loaded - 503: Unable to connect to OGX + """ _ = auth _ = request @@ -964,6 +975,7 @@ async def delete_vector_store_file( Returns: VectorStoreFileDeleteResponse: Delete outcome for the requested file. + """ _ = auth _ = request diff --git a/src/app/routers.py b/src/app/routers.py index f13de514f..05a131663 100644 --- a/src/app/routers.py +++ b/src/app/routers.py @@ -47,6 +47,7 @@ def include_routers(app: FastAPI) -> None: Parameters: ---------- app (FastAPI): The FastAPI application to which routers will be attached. + """ app.include_router(root.router) diff --git a/src/authentication/__init__.py b/src/authentication/__init__.py index 44e8ddc42..172ef94f7 100644 --- a/src/authentication/__init__.py +++ b/src/authentication/__init__.py @@ -37,6 +37,7 @@ def get_auth_dependency( # pylint: disable=too-many-return-statements Raises: ------ ValueError: If the configured authentication module is not supported. + """ try: module = configuration.authentication_configuration.module diff --git a/src/authentication/api_key_token.py b/src/authentication/api_key_token.py index bff68633f..269b5c8f0 100644 --- a/src/authentication/api_key_token.py +++ b/src/authentication/api_key_token.py @@ -54,6 +54,7 @@ def __init__( Args: config: The API key token configuration containing the API key. virtual_path: The virtual path for the service (default: DEFAULT_VIRTUAL_PATH). + """ self.virtual_path: str = virtual_path self.config: APIKeyTokenConfiguration = config @@ -72,6 +73,7 @@ async def __call__(self, request: Request) -> AuthTuple: Raises: HTTPException: If the bearer token is missing or doesn't match the configured API key (HTTP 401). + """ if not request.headers.get("Authorization"): if _should_skip_auth(request): diff --git a/src/authentication/interface.py b/src/authentication/interface.py index 545623bb1..d43f373fd 100644 --- a/src/authentication/interface.py +++ b/src/authentication/interface.py @@ -45,4 +45,5 @@ async def __call__(self, request: Request) -> AuthTuple: skip_user_id_check (bool): whether downstream handlers should skip user-id verification, token (str): authentication token or NO_USER_TOKEN when no token is present. + """ diff --git a/src/authentication/jwk_token.py b/src/authentication/jwk_token.py index 2038427ae..79b59c4f5 100644 --- a/src/authentication/jwk_token.py +++ b/src/authentication/jwk_token.py @@ -45,6 +45,7 @@ async def get_jwk_set(url: str) -> KeySet: Returns: KeySet: The JWK `KeySet` corresponding to the URL. + """ async with _jwk_cache_lock: if url not in _jwk_cache: @@ -81,6 +82,7 @@ def key_resolver_func( callable: A function `header, payload -> Key` that resolves and returns the verification key. Raises `KeyNotFoundError` when resolution fails. + """ def _internal(header: dict[str, Any], _payload: dict[str, Any]) -> Key: @@ -100,6 +102,7 @@ def _internal(header: dict[str, Any], _payload: dict[str, Any]) -> Key: KeyNotFoundError: If the header is missing `alg`, no matching key is found, multiple keys match a `kid`, or a found key's algorithm does not match the header's `alg`. + """ if "alg" not in header: raise KeyNotFoundError("Token header missing 'alg' field") @@ -175,6 +178,7 @@ def __init__( Notes: ----- Initializes the instance and sets `skip_userid_check` to False. + """ self.virtual_path: str = virtual_path self.config: JwkConfiguration = config @@ -204,6 +208,7 @@ async def __call__( # pylint: disable=too-many-statements AuthTuple: A tuple (user_id, username, skip_userid_check, token) extracted from the validated JWT. Only returned on successful authentication; all error paths raise HTTPException. + """ if not request.headers.get("Authorization"): if _should_skip_auth(request): diff --git a/src/authentication/k8s.py b/src/authentication/k8s.py index 336b8d5e5..912bf56e4 100644 --- a/src/authentication/k8s.py +++ b/src/authentication/k8s.py @@ -95,6 +95,7 @@ def __new__(cls: type[Self]) -> Self: Returns: instance (K8sClientSingleton): The singleton instance. + """ if cls._instance is None: cls._instance = super().__new__(cls) @@ -152,6 +153,7 @@ def get_authn_api(cls) -> kubernetes.client.AuthenticationV1Api: Returns: kubernetes.client.AuthenticationV1Api: The initialized AuthenticationV1Api instance. + """ if cls._instance is None or cls._authn_api is None: cls() @@ -166,6 +168,7 @@ def get_authz_api(cls) -> kubernetes.client.AuthorizationV1Api: Returns: kubernetes.client.AuthorizationV1Api: The Kubernetes Authorization API client instance. + """ if cls._instance is None or cls._authz_api is None: cls() @@ -179,6 +182,7 @@ def get_custom_objects_api(cls) -> kubernetes.client.CustomObjectsApi: Returns: kubernetes.client.CustomObjectsApi: The CustomObjectsApi client used by the singleton. + """ if cls._instance is None or cls._custom_objects_api is None: cls() @@ -201,6 +205,7 @@ def _get_cluster_id(cls) -> str: ClusterVersionNotFoundError: If the ClusterVersion resource does not exist (404). ClusterVersionPermissionError: If access to ClusterVersion is denied (403). InvalidClusterVersionError: If ClusterVersion has invalid structure or missing fields. + """ try: custom_objects_api = cls.get_custom_objects_api() @@ -291,6 +296,7 @@ def get_cluster_id(cls) -> str: ClusterVersionNotFoundError: If the ClusterVersion resource does not exist. ClusterVersionPermissionError: If access to ClusterVersion is denied. InvalidClusterVersionError: If ClusterVersion has invalid structure. + """ if cls._instance is None: cls() @@ -320,6 +326,7 @@ def get_user_info(token: str) -> Optional[kubernetes.client.V1TokenReviewStatus] 503 if Kubernetes API is unavailable (5xx errors, 429 rate limit). 503 if unable to initialize Kubernetes client. 500 if Kubernetes API configuration issue (4xx errors). + """ try: auth_api = K8sClientSingleton.get_authn_api() @@ -416,6 +423,7 @@ def __init__(self, virtual_path: str = DEFAULT_VIRTUAL_PATH) -> None: virtual_path: Stored `virtual_path` value. skip_userid_check (bool): Flag indicating whether user ID checks should be skipped; initialized to False. + """ self.virtual_path = virtual_path self.skip_userid_check = False @@ -437,6 +445,7 @@ async def __call__(self, request: Request) -> AuthTuple: Raises: ------ HTTPException: If authentication or authorization fails. + """ # LCORE-694: Config option to skip authorization for readiness and liveness probe if not request.headers.get("Authorization"): diff --git a/src/authentication/noop.py b/src/authentication/noop.py index 6d32f45c3..236d7aef2 100644 --- a/src/authentication/noop.py +++ b/src/authentication/noop.py @@ -27,6 +27,7 @@ def __init__(self, virtual_path: str = DEFAULT_VIRTUAL_PATH) -> None: ---------- virtual_path (str): Virtual path context used by the dependency (defaults to DEFAULT_VIRTUAL_PATH). + """ self.virtual_path = virtual_path self.skip_userid_check = True @@ -46,6 +47,7 @@ async def __call__(self, request: Request) -> AuthTuple: - username: DEFAULT_USER_NAME. - skip_userid_check: True to indicate the user ID check is skipped. - token: NO_USER_TOKEN. + """ logger.warning( "No-op authentication dependency is being used. " diff --git a/src/authentication/noop_with_token.py b/src/authentication/noop_with_token.py index 0656d952a..cf39bd786 100644 --- a/src/authentication/noop_with_token.py +++ b/src/authentication/noop_with_token.py @@ -39,6 +39,7 @@ def __init__(self, virtual_path: str = DEFAULT_VIRTUAL_PATH) -> None: Notes: ----- Sets the instance attribute `virtual_path` and sets `skip_userid_check` to True. + """ self.virtual_path = virtual_path self.skip_userid_check = True @@ -58,6 +59,7 @@ async def __call__(self, request: Request) -> AuthTuple: - username: DEFAULT_USER_NAME. - skip_userid_check: True to indicate user-id checks are skipped. - user_token: Token extracted from the request headers. + """ logger.warning( "No-op with token authentication dependency is being used. " diff --git a/src/authentication/rh_identity.py b/src/authentication/rh_identity.py index e0d45d975..2ba87c2db 100644 --- a/src/authentication/rh_identity.py +++ b/src/authentication/rh_identity.py @@ -56,6 +56,7 @@ def __init__( Raises: HTTPException: If validation fails (400 for format errors, 403 for entitlements) + """ self.identity_data = identity_data self.required_entitlements = required_entitlements or [] @@ -66,6 +67,7 @@ def _validate_structure(self) -> None: Raises: HTTPException: 400 if required fields are missing or malformed + """ if ( "identity" not in self.identity_data @@ -104,6 +106,7 @@ def _validate_user_fields(self, identity: dict) -> None: Raises: HTTPException: 400 if required User fields are missing or malformed + """ if "user" not in identity: logger.warning( @@ -130,6 +133,7 @@ def _validate_system_fields(self, identity: dict) -> None: Raises: HTTPException: 400 if required System fields are missing or malformed + """ if "system" not in identity: logger.warning( @@ -166,6 +170,7 @@ def _validate_service_account_fields(self, identity: dict) -> None: Raises: HTTPException: 400 if required ServiceAccount fields are missing or malformed + """ if "service_account" not in identity: logger.warning( @@ -195,6 +200,7 @@ def _validate_string_field( Raises: HTTPException: 400 if validation fails + """ if not isinstance(value, str): logger.warning( @@ -228,6 +234,7 @@ def _get_identity_type(self) -> str: Returns: Identity type string + """ return self.identity_data["identity"]["type"] @@ -237,6 +244,7 @@ def get_user_id(self) -> str: Returns: User ID (user.user_id for User type, system.cn for System type, service_account.client_id for ServiceAccount type) + """ identity = self.identity_data["identity"] identity_type = self._get_identity_type() @@ -259,6 +267,7 @@ def get_username(self) -> str: Username (user.username for User type; service_account.username for ServiceAccount type; account_number or system.cn for System type) + """ identity = self.identity_data["identity"] identity_type = self._get_identity_type() @@ -279,6 +288,7 @@ def get_org_id(self) -> str: Returns: Organization ID string, or empty string if not present + """ return self.identity_data["identity"].get("org_id", "") @@ -287,6 +297,7 @@ def get_system_id(self) -> str: Returns: System ID string for System identities, or empty string for User identities + """ if self._get_identity_type() == "System": return self.identity_data["identity"]["system"]["cn"] @@ -300,6 +311,7 @@ def has_entitlement(self, service: str) -> bool: Returns: True if user has the entitlement and is_entitled is True + """ entitlements = self.identity_data.get("entitlements", {}) service_entitlement = entitlements.get(service, {}) @@ -313,6 +325,7 @@ def has_entitlements(self, services: list[str]) -> bool: Returns: True if user has ALL entitlements in the list + """ return all(self.has_entitlement(service) for service in services) @@ -321,6 +334,7 @@ def validate_entitlements(self) -> None: Raises: HTTPException: 403 if required entitlements are missing + """ if not self.required_entitlements: return # No validation required @@ -359,6 +373,7 @@ def __init__( max_header_size: Maximum allowed size in bytes for the base64-encoded x-rh-identity header. Headers exceeding this size are rejected before decoding. + """ self.required_entitlements = required_entitlements self.virtual_path = virtual_path @@ -379,6 +394,7 @@ async def __call__(self, request: Request) -> AuthTuple: - 401: Missing x-rh-identity header - 400: Invalid base64, invalid JSON, or missing required fields - 403: Missing required entitlements + """ # Extract header identity_header = request.headers.get("x-rh-identity") diff --git a/src/authentication/trusted_proxy.py b/src/authentication/trusted_proxy.py index c0129e9f2..8460bf8df 100644 --- a/src/authentication/trusted_proxy.py +++ b/src/authentication/trusted_proxy.py @@ -44,6 +44,7 @@ def __init__( and optional SA allowlist. virtual_path: The request path used for authorization checks; defaults to DEFAULT_VIRTUAL_PATH. + """ self.config = config self.virtual_path = virtual_path @@ -63,6 +64,7 @@ async def __call__(self, request: Request) -> AuthTuple: Raises: ------ HTTPException: If authentication fails. + """ if not request.headers.get("Authorization"): if configuration.authentication_configuration.skip_for_health_probes: diff --git a/src/authentication/utils.py b/src/authentication/utils.py index aad460c00..4901cc12b 100644 --- a/src/authentication/utils.py +++ b/src/authentication/utils.py @@ -21,6 +21,7 @@ def extract_user_token(headers: Headers) -> str: Raises: ------ HTTPException: If the Authorization header is missing or malformed. + """ authorization_header = headers.get("Authorization") if not authorization_header: diff --git a/src/authorization/azure_token_manager.py b/src/authorization/azure_token_manager.py index 702efe465..48ebe0434 100644 --- a/src/authorization/azure_token_manager.py +++ b/src/authorization/azure_token_manager.py @@ -71,6 +71,7 @@ def build_azure_provider_data(self) -> Optional[dict[str, str]]: Returns: Provider data dict when a token and base_url are available. + """ token = self.access_token.get_secret_value() if not token or self.azure_base_url is None: @@ -85,6 +86,7 @@ def refresh_token(self) -> bool: Raises: ValueError: If Entra ID configuration has not been set. + """ if self._entra_id_config is None: raise ValueError("Azure Entra ID configuration not set") diff --git a/src/authorization/middleware.py b/src/authorization/middleware.py index 93cb5e7e0..a0188ac02 100644 --- a/src/authorization/middleware.py +++ b/src/authorization/middleware.py @@ -43,6 +43,7 @@ def get_authorization_resolvers() -> tuple[RolesResolver, AccessResolver]: Returns: tuple[RolesResolver, AccessResolver]: (roles_resolver, access_resolver) appropriate for the current configuration. + """ authorization_cfg = configuration.authorization_configuration authentication_config = configuration.authentication_configuration @@ -123,6 +124,7 @@ async def _perform_authorization_check( HTTPException: with 500 Internal Server Error if `auth` is missing from `kwargs`. HTTPException: with 403 Forbidden if the resolved roles are not permitted to perform `action`. + """ role_resolver, access_resolver = get_authorization_resolvers() @@ -174,6 +176,7 @@ def authorize(action: Action) -> Callable: Callable: A decorator which, when applied to an endpoint function, performs the authorization check for the given action before invoking the function. + """ def decorator(func: Callable) -> Callable: @@ -187,6 +190,7 @@ def decorator(func: Callable) -> Callable: Returns: ------- Callable: A wrapper that performs authorization then calls `func`. + """ @wraps(func) diff --git a/src/authorization/resolvers.py b/src/authorization/resolvers.py index b848f8f34..74f0aa423 100644 --- a/src/authorization/resolvers.py +++ b/src/authorization/resolvers.py @@ -39,6 +39,7 @@ async def resolve_roles(self, auth: AuthTuple) -> UserRoles: Returns: ------- UserRoles: A set of role names associated with the authenticated subject. + """ @@ -54,6 +55,7 @@ async def resolve_roles(self, auth: AuthTuple) -> UserRoles: Returns: An empty set of role names. + """ _ = auth # Unused return set() @@ -67,6 +69,7 @@ def unsafe_get_claims(token: str) -> dict[str, Any]: Returns: dict[str, Any]: Claims dictionary parsed from the JWT payload. + """ payload = token.split(".")[1] padded = payload + "=" * (-len(payload) % 4) @@ -87,6 +90,7 @@ def __init__(self, role_rules: list[JwtRoleRule]): claim matches to roles. Each rule specifies a JSONPath to evaluate, an operator to apply to matches, the roles to grant when the rule matches, and optional negation or regex matching. + """ self.role_rules = role_rules @@ -99,6 +103,7 @@ async def resolve_roles(self, auth: AuthTuple) -> UserRoles: Returns: roles (UserRoles): Set of role names derived from all configured rules that match the token's claims. + """ jwt_claims = self._get_claims(auth) return { @@ -124,6 +129,7 @@ def evaluate_role_rules(rule: JwtRoleRule, jwt_claims: dict[str, Any]) -> UserRo ------- roles (set[str]): The set of roles from `rule.roles` if the rule matches `jwt_claims`, otherwise an empty set. + """ return ( set(rule.roles) @@ -149,6 +155,7 @@ def _get_claims(auth: AuthTuple) -> dict[str, Any]: dict[str, Any]: Decoded JWT claims as a dictionary. Returns an empty dict when the token equals constants.NO_USER_TOKEN (guest). The token payload is decoded without validating the JWT signature. + """ _, _, _, token = auth if token == constants.NO_USER_TOKEN: @@ -184,6 +191,7 @@ def _evaluate_operator( ------- bool: `true` if the operator evaluation (after applying negation when set) succeeds, `false` otherwise. + """ result = False match rule.operator: @@ -225,6 +233,7 @@ def check_access(self, action: Action, user_roles: UserRoles) -> bool: ------- bool: `true` if at least one role in `user_roles` grants the requested `action`, `false` otherwise. + """ @abstractmethod @@ -242,6 +251,7 @@ def get_actions(self, user_roles: UserRoles) -> set[Action]: set[Action]: The aggregated set of allowed actions for the given roles. If `ADMIN` is included in the aggregated actions, returns all available non-`ADMIN` actions. + """ @@ -261,6 +271,7 @@ def check_access(self, action: Action, user_roles: UserRoles) -> bool: Returns: ------- `true` always (access is always granted). + """ _ = action # We're noop, it doesn't matter, everyone is allowed _ = user_roles # We're noop, it doesn't matter, everyone is allowed @@ -273,6 +284,7 @@ def get_actions(self, user_roles: UserRoles) -> set[Action]: Returns: allowed_actions (set[Action]): All defined `Action` values except `Action.ADMIN`. + """ _ = user_roles # We're noop, it doesn't matter, everyone is allowed return set(Action) - {Action.ADMIN} @@ -300,6 +312,7 @@ def __init__(self, access_rules: list[AccessRule]): ------ ValueError: If any rule contains the `Action.ADMIN` action together with other actions. + """ for rule in access_rules: # Since this is nonsensical, it might be a mistake, so hard fail @@ -334,6 +347,7 @@ def check_access(self, action: Action, user_roles: UserRoles) -> bool: ------- true if at least one role permits the action or ADMIN override applies, false otherwise. + """ if action != Action.ADMIN and self.check_access(Action.ADMIN, user_roles): # Recurse to check if the roles allow the user to perform the admin action, @@ -361,6 +375,7 @@ def get_actions(self, user_roles: UserRoles) -> set[Action]: allowed_actions (set[Action]): Set of actions the user may perform. If any role grants Action.ADMIN, returns every Action except Action.ADMIN. + """ actions = { action diff --git a/src/cache/cache.py b/src/cache/cache.py index afaf8b355..6fdf1590b 100644 --- a/src/cache/cache.py +++ b/src/cache/cache.py @@ -36,6 +36,7 @@ def _check_user_id(user_id: str, skip_user_id_check: bool) -> None: Raises: ------ ValueError: If validation is not skipped and `user_id` is invalid. + """ if skip_user_id_check: return @@ -54,6 +55,7 @@ def _check_conversation_id(conversation_id: str) -> None: ------ ValueError: If `conversation_id` is not a valid SUID (UUID-format string; dashes are optional). + """ if not check_suid(conversation_id): raise ValueError(f"Invalid conversation ID {conversation_id}") @@ -75,6 +77,7 @@ def construct_key( Returns: ------- str: Compound key in the form "user_id:conversation_id". + """ Cache._check_user_id(user_id, skip_user_id_check) Cache._check_conversation_id(conversation_id) @@ -98,6 +101,7 @@ def get( ------- list[CacheEntry]: List of cache entries for the specified key; empty list if no entries exist. + """ @abstractmethod @@ -119,6 +123,7 @@ def insert_or_append( conversation_id (str): Identifier of the conversation within the user's scope. cache_entry (CacheEntry): Cache entry to store or append. skip_user_id_check (bool): If True, skip validation of `user_id`. + """ @abstractmethod @@ -136,6 +141,7 @@ def delete( Returns: ------- `True` if entries were deleted, `False` if no key was found. + """ @abstractmethod @@ -152,6 +158,7 @@ def list(self, user_id: str, skip_user_id_check: bool) -> list[ConversationData] list[ConversationData]: A list of ConversationData objects for the user's conversations, each containing `conversation_id`, `topic_summary`, and `last_message_timestamp`. + """ @abstractmethod @@ -170,6 +177,7 @@ def set_topic_summary( conversation_id (str): Conversation identifier used as part of the compound cache key. topic_summary (str): Text summary of the conversation topic to store. skip_user_id_check (bool): If True, skip validation of `user_id` before storing. + """ @abstractmethod @@ -194,6 +202,7 @@ def store_summary( conversation_id (str): Conversation identifier, part of the compound key. summary (ConversationSummary): The summary chunk to persist. skip_user_id_check (bool): If True, skip validation of `user_id`. + """ @abstractmethod @@ -212,6 +221,7 @@ def get_summaries( ------- list[ConversationSummary]: Summary chunks for the conversation, oldest first (ordered by ``created_at``); empty list if none exist. + """ @abstractmethod @@ -238,6 +248,7 @@ def replace_summaries( folded_summary (ConversationSummary): The folded summary that supersedes the conversation's existing chunks. skip_user_id_check (bool): If True, skip validation of `user_id`. + """ @abstractmethod @@ -246,4 +257,5 @@ def ready(self) -> bool: Returns: True if the cache is ready, False otherwise. + """ diff --git a/src/cache/cache_entry.py b/src/cache/cache_entry.py index f0768eb4a..022edf35b 100644 --- a/src/cache/cache_entry.py +++ b/src/cache/cache_entry.py @@ -22,6 +22,7 @@ class CacheEntry(BaseModel): referenced_documents: List of documents referenced by the response tool_calls: List of tool calls made during response generation tool_results: List of tool results from tool calls + """ query: str diff --git a/src/cache/cache_factory.py b/src/cache/cache_factory.py index cbc066a29..efd9c14b5 100644 --- a/src/cache/cache_factory.py +++ b/src/cache/cache_factory.py @@ -27,6 +27,7 @@ def conversation_cache(config: ConversationHistoryConfiguration) -> Cache: ValueError: If `config.type` is None, if required type-specific configuration is missing, or if `config.type` is not a recognized cache type. + """ logger.info("Creating cache instance of type %s", config.type) match config.type: diff --git a/src/cache/in_memory_cache.py b/src/cache/in_memory_cache.py index 302893a42..d3a0543e2 100644 --- a/src/cache/in_memory_cache.py +++ b/src/cache/in_memory_cache.py @@ -24,6 +24,7 @@ def __init__(self, config: InMemoryCacheConfig) -> None: Parameters: ---------- config (InMemoryCacheConfig): Configuration options controlling cache behavior. + """ self.cache_config = config @@ -46,6 +47,7 @@ def connected(self) -> bool: Returns: True if the cache is available, False otherwise. + """ return True @@ -74,6 +76,7 @@ def get( Returns: ------- Empty list. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -101,6 +104,7 @@ def insert_or_append( conversation_id: Conversation ID unique for given user. cache_entry: The `CacheEntry` object to store. skip_user_id_check: Skip user_id suid check. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -122,6 +126,7 @@ def delete( Returns: ------- bool: True in all cases. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -162,6 +167,7 @@ def set_topic_summary( conversation_id: Conversation ID unique for given user. topic_summary: The topic summary to store. skip_user_id_check: Skip user_id suid check. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -185,6 +191,7 @@ def store_summary( conversation_id: Conversation ID unique for given user. summary: The ConversationSummary chunk (not persisted). skip_user_id_check: Skip user_id suid check. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -204,6 +211,7 @@ def get_summaries( Returns: ------- An empty list. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -228,6 +236,7 @@ def replace_summaries( conversation_id: Conversation ID unique for given user. folded_summary: The folded summary (not persisted). skip_user_id_check: Skip user_id suid check. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -237,5 +246,6 @@ def ready(self) -> bool: Returns: True (`bool`): Always `True` for this in-memory cache implementation. + """ return True diff --git a/src/cache/noop_cache.py b/src/cache/noop_cache.py index a0bbb017d..971b33e22 100644 --- a/src/cache/noop_cache.py +++ b/src/cache/noop_cache.py @@ -32,6 +32,7 @@ def connected(self) -> bool: Returns: `true` if the cache is connected, `false` otherwise. + """ return True @@ -57,6 +58,7 @@ def get( Returns: ------- list[CacheEntry]: An empty list (this cache does not persist entries). + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -81,6 +83,7 @@ def insert_or_append( conversation_id: Conversation ID unique for given user. cache_entry: The `CacheEntry` object to store. skip_user_id_check: Skip user_id suid check. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -100,6 +103,7 @@ def delete( Returns: ------- bool: True in all cases. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -121,6 +125,7 @@ def list( Returns: ------- An empty list. + """ super()._check_user_id(user_id, skip_user_id_check) return [] @@ -141,6 +146,7 @@ def set_topic_summary( conversation_id: Conversation ID unique for given user. topic_summary: The topic summary to store. skip_user_id_check: Skip user_id suid check. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -161,6 +167,7 @@ def store_summary( conversation_id: Conversation ID unique for given user. summary: The ConversationSummary chunk (not persisted). skip_user_id_check: Skip user_id suid check. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -180,6 +187,7 @@ def get_summaries( Returns: ------- An empty list. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -201,6 +209,7 @@ def replace_summaries( conversation_id: Conversation ID unique for given user. folded_summary: The folded summary (not persisted). skip_user_id_check: Skip user_id suid check. + """ # just check if user_id and conversation_id are UUIDs super().construct_key(user_id, conversation_id, skip_user_id_check) @@ -210,5 +219,6 @@ def ready(self) -> bool: Returns: True in all cases. + """ return True diff --git a/src/cache/postgres_cache.py b/src/cache/postgres_cache.py index ea0661a3d..dcd84b1a2 100644 --- a/src/cache/postgres_cache.py +++ b/src/cache/postgres_cache.py @@ -179,6 +179,7 @@ def __init__(self, config: PostgreSQLDatabaseConfiguration) -> None: ---------- config (PostgreSQLDatabaseConfiguration): Configuration used to connect to the PostgreSQL server and configure the cache. + """ self.postgres_config = config @@ -233,6 +234,7 @@ def connected(self) -> bool: Returns: bool: `True` if the connection exists and a simple probe query succeeds, `False` otherwise. + """ if self.connection is None: logger.warning("Not connected, need to reconnect later") @@ -257,6 +259,7 @@ def initialize_cache(self, namespace: str) -> None: Raises: ------ CacheError: If the cache is disconnected. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -305,6 +308,7 @@ def get( # pylint: disable=R0914 Raises: ------ CacheError: If the cache connection is not available. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -402,6 +406,7 @@ def insert_or_append( ------ CacheError: If the cache is disconnected or a database error occurs while persisting the entry. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -500,6 +505,7 @@ def delete( Raises: ------ CacheError: If the cache is disconnected or a database error occurs. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -582,6 +588,7 @@ def set_topic_summary( conversation_id: Conversation ID unique for given user. topic_summary: The topic summary to store. skip_user_id_check: Skip user_id suid check. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -617,6 +624,7 @@ def store_summary( Raises: ------ CacheError: If the cache is disconnected or a database error occurs. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -660,6 +668,7 @@ def get_summaries( Raises: ------ CacheError: If the cache is disconnected or a database error occurs. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -711,6 +720,7 @@ def replace_summaries( Raises: ------ CacheError: If the cache is disconnected or a database error occurs. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -743,5 +753,6 @@ def ready(self) -> bool: Returns: True if the cache is ready, False otherwise. + """ return True diff --git a/src/cache/sqlite_cache.py b/src/cache/sqlite_cache.py index 829be38b4..5c347b232 100644 --- a/src/cache/sqlite_cache.py +++ b/src/cache/sqlite_cache.py @@ -173,6 +173,7 @@ def __init__(self, config: SQLiteDatabaseConfiguration) -> None: config (SQLiteDatabaseConfiguration): Configuration containing the SQLite database path and related settings used to create and open the connection. + """ self.sqlite_config = config @@ -190,6 +191,7 @@ def connect(self) -> None: Raises: sqlite3.Error: If the database cannot be opened or the cache schema cannot be initialized. + """ logger.info("Connecting to storage") # make sure the connection will have known state @@ -213,6 +215,7 @@ def connected(self) -> bool: Returns: bool: `True` if a working connection to storage exists, `False` otherwise. + """ if self.connection is None: logger.warning("Not connected, need to reconnect later") @@ -247,6 +250,7 @@ def initialize_cache(self) -> None: Raises: CacheError: if the database connection is not established. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -288,6 +292,7 @@ def get( # pylint: disable=R0914 Raises: ------ CacheError: If the cache connection is disconnected. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -385,6 +390,7 @@ def insert_or_append( Raises: ------ CacheError: If the cache connection is not available. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -482,6 +488,7 @@ def delete( Raises: ------ CacheError: If the cache connection is not available. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -563,6 +570,7 @@ def set_topic_summary( conversation_id: Conversation ID unique for given user. topic_summary: The topic summary to store. skip_user_id_check: Skip user_id suid check. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -596,6 +604,7 @@ def store_summary( Raises: ------ CacheError: If the cache connection is not available. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -637,6 +646,7 @@ def get_summaries( Raises: ------ CacheError: If the cache connection is not available. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -683,6 +693,7 @@ def replace_summaries( Raises: ------ CacheError: If the cache connection is not available. + """ if self.connection is None: logger.error("Cache is disconnected") @@ -710,5 +721,6 @@ def ready(self) -> bool: Returns: True if the cache is ready, False otherwise. + """ return True diff --git a/src/client/ogx.py b/src/client/ogx.py index 8f07169c6..5d87b6234 100644 --- a/src/client/ogx.py +++ b/src/client/ogx.py @@ -42,6 +42,7 @@ def read_provider_data(client: AsyncOgxClient) -> dict[str, Any]: Returns: A mutable copy of the current provider data dict (empty if unset). + """ if isinstance(client, AsyncOGXAsLibraryClient): return dict(client.provider_data or {}) @@ -202,6 +203,7 @@ def get_client(self) -> AsyncOgxClient: Raises: RuntimeError: If the client has not been initialized; call `load(...)` first. + """ if not self._lsc: raise RuntimeError( @@ -240,6 +242,7 @@ async def upload_file( Raises: RuntimeError: If in library mode but the client has not finished initializing. + """ client = self.get_client() if self.is_library_client: @@ -270,6 +273,7 @@ async def reload_library_client(self) -> AsyncOgxClient: Returns: The reloaded client instance. + """ if not self._config_path: raise RuntimeError("Cannot reload: config path not set") @@ -311,6 +315,7 @@ async def check_model_available(self, model_id: str) -> tuple[bool, str]: Returns: A tuple of (available, reason) where available is True if the model was found, and reason describes the outcome. + """ try: client = self.get_client() @@ -363,6 +368,7 @@ async def update_azure_token(self) -> AsyncOgxClient: Returns: The new client instance assigned to this holder. + """ updates = AzureEntraIDManager().build_azure_provider_data() if not updates: @@ -408,6 +414,7 @@ async def get_azure_base_url(self) -> Optional[str]: Returns: Optional[str]: The Azure base_url if available, otherwise None. + """ if not self._lsc: return None diff --git a/src/configuration.py b/src/configuration.py index 4e2750d61..9298f9e44 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -66,6 +66,7 @@ def replace_env_vars_preserving_native_override( Returns: dict[Any, Any]: The config with env refs resolved everywhere except inside native_override. + """ if not isinstance(config_dict, dict): return replace_env_vars(config_dict) @@ -123,6 +124,7 @@ def load_configuration(self, filename: str) -> None: Parameters: ---------- filename (str): Path to the YAML configuration file to load. + """ with open(filename, encoding="utf-8") as fin: config_dict = yaml.safe_load(fin) @@ -140,6 +142,7 @@ def init_from_dict(self, config_dict: dict[Any, Any]) -> None: Configuration(**config_dict) and clears any cached conversation cache, quota limiters, and token usage history so they will be reinitialized on next access. + """ # clear cached values when configuration changes self._conversation_cache = None @@ -157,6 +160,7 @@ def configuration(self) -> Configuration: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -171,6 +175,7 @@ def service_configuration(self) -> ServiceConfiguration: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -185,6 +190,7 @@ def ogx_configuration(self) -> OgxConfiguration: Raises: LogicError: If the application configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -200,6 +206,7 @@ def user_data_collection_configuration(self) -> UserDataCollection: Raises: LogicError: If the application configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -214,6 +221,7 @@ def mcp_servers(self) -> list[ModelContextProtocolServer]: Raises: LogicError: If the configuration is not loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -225,6 +233,7 @@ def dynamic_mcp_server_names(self) -> set[str]: Returns: set[str]: Names of MCP servers added via the API (not from config file). + """ return self._dynamic_mcp_server_names @@ -239,6 +248,7 @@ def add_mcp_server(self, mcp_server: ModelContextProtocolServer) -> None: ------ LogicError: If the configuration has not been loaded. ValueError: If an MCP server with the same name already exists. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -261,6 +271,7 @@ def remove_mcp_server(self, name: str) -> None: ------ LogicError: If the configuration has not been loaded. ValueError: If the server was not found or was statically configured. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -283,6 +294,7 @@ def is_dynamic_mcp_server(self, name: str) -> bool: Returns: ------- bool: True if the server was registered via the API. + """ return name in self._dynamic_mcp_server_names @@ -296,6 +308,7 @@ def authentication_configuration(self) -> AuthenticationConfiguration: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -313,6 +326,7 @@ def authorization_configuration(self) -> AuthorizationConfiguration: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -332,6 +346,7 @@ def customization(self) -> Optional[Customization]: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -347,6 +362,7 @@ def rlsapi_v1(self) -> RlsapiV1Configuration: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -362,6 +378,7 @@ def inference(self) -> InferenceConfiguration: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -377,6 +394,7 @@ def compaction(self) -> CompactionConfiguration: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -392,6 +410,7 @@ def conversation_cache_configuration(self) -> ConversationHistoryConfiguration: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -406,6 +425,7 @@ def database_configuration(self) -> DatabaseConfiguration: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -420,6 +440,7 @@ def quota_handlers_configuration(self) -> QuotaHandlersConfiguration: Raises: LogicError: If configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -441,6 +462,7 @@ def conversation_cache(self) -> Cache: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -459,6 +481,7 @@ def quota_limiters(self) -> list[QuotaLimiter]: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -483,6 +506,7 @@ def token_usage_history(self) -> Optional[TokenUsageHistory]: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -532,6 +556,7 @@ def approvals_configuration(self) -> ApprovalsConfiguration: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -575,6 +600,7 @@ def rag_id_mapping(self) -> dict[str, str]: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -603,6 +629,7 @@ def score_multiplier_mapping(self) -> dict[str, float]: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -621,6 +648,7 @@ def relevance_cutoff_mapping(self) -> dict[str, float]: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -638,6 +666,7 @@ def inline_solr_enabled(self) -> bool: Raises: LogicError: If the configuration has not been loaded. + """ if self._configuration is None: raise LogicError("logic error: configuration is not loaded") @@ -659,6 +688,7 @@ def resolve_index_name( Returns: ------- str: The user-facing index name from config, or the original ID. + """ mapping = rag_id_mapping if rag_id_mapping is not None else self.rag_id_mapping return mapping.get(vector_store_id, vector_store_id) diff --git a/src/lightspeed_stack.py b/src/lightspeed_stack.py index 01ae41e20..fcdec16c3 100644 --- a/src/lightspeed_stack.py +++ b/src/lightspeed_stack.py @@ -39,6 +39,7 @@ def create_argument_parser() -> ArgumentParser: Returns: Configured ArgumentParser for parsing the service CLI options. + """ parser = ArgumentParser() parser.add_argument( @@ -155,6 +156,7 @@ def main() -> None: Raises: SystemExit: when configuration dumping or migration fails (exits with status 1). + """ logger.info("Lightspeed Core Stack startup") parser = create_argument_parser() diff --git a/src/log.py b/src/log.py index 39e3414ab..476bb2a88 100644 --- a/src/log.py +++ b/src/log.py @@ -55,6 +55,7 @@ def resolve_log_level() -> int: Returns: ------- int: A valid logging level constant (e.g., logging.INFO, logging.DEBUG). + """ level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL) diff --git a/src/metrics/recording.py b/src/metrics/recording.py index c41c105b5..4b34e73fe 100644 --- a/src/metrics/recording.py +++ b/src/metrics/recording.py @@ -21,6 +21,7 @@ def measure_response_duration(path: str) -> Iterator[None]: Args: path: Normalized route path used as the metric label. + """ try: cm = metrics.response_duration_seconds.labels(path).time() @@ -38,6 +39,7 @@ def record_rest_api_call(path: str, status_code: int) -> None: Args: path: Normalized route path used as the metric label. status_code: HTTP response status code returned by the endpoint. + """ try: metrics.rest_api_calls_total.labels(path, status_code).inc() @@ -52,6 +54,7 @@ def record_llm_call(provider: str, model: str, endpoint_path: str) -> None: provider: LLM provider identifier. model: LLM model identifier without the provider prefix. endpoint_path: The API endpoint path for metric labeling. + """ try: metrics.llm_calls_total.labels(provider, model, endpoint_path).inc() @@ -66,6 +69,7 @@ def record_llm_failure(provider: str, model: str, endpoint_path: str) -> None: provider: LLM provider identifier. model: LLM model identifier without the provider prefix. endpoint_path: The API endpoint path for metric labeling. + """ try: metrics.llm_calls_failures_total.labels(provider, model, endpoint_path).inc() @@ -78,6 +82,7 @@ def record_llm_validation_error(endpoint_path: str = "") -> None: Args: endpoint_path: The API endpoint path for metric labeling. + """ try: metrics.llm_calls_validation_errors_total.labels(endpoint_path).inc() @@ -100,6 +105,7 @@ def record_llm_token_usage( input_tokens: Number of tokens sent to the LLM. output_tokens: Number of tokens received from the LLM. endpoint_path: The API endpoint path for metric labeling. + """ try: metrics.llm_token_sent_total.labels(provider, model, endpoint_path).inc( @@ -130,6 +136,7 @@ def normalize_llm_inference_result(result: str) -> str: Returns: A value guaranteed to be in ``ALLOWED_LLM_INFERENCE_RESULTS``. + """ return ( result @@ -149,6 +156,7 @@ def record_llm_inference_duration( endpoint_path: API endpoint path for metric labeling. result: Bounded result label, such as ``success`` or ``failure``. duration: Inference call duration in seconds. + """ bounded_result = normalize_llm_inference_result(result) try: @@ -167,6 +175,7 @@ def set_started_in_degraded_mode(is_degraded: bool) -> None: Args: is_degraded: True if service started in degraded mode, False if healthy. + """ try: metrics.started_in_degraded_mode.set(1 if is_degraded else 0) diff --git a/src/models/api/requests/catalog.py b/src/models/api/requests/catalog.py index 2a90e79ec..defd4e6ac 100644 --- a/src/models/api/requests/catalog.py +++ b/src/models/api/requests/catalog.py @@ -10,6 +10,7 @@ class ModelFilter(BaseModel): Attributes: model_type: Required model type, such as 'llm', 'embeddings' etc. + """ model_config = {"extra": "forbid"} diff --git a/src/models/api/requests/conversations.py b/src/models/api/requests/conversations.py index 3c659f952..e482317e6 100644 --- a/src/models/api/requests/conversations.py +++ b/src/models/api/requests/conversations.py @@ -8,6 +8,7 @@ class ConversationUpdateRequest(BaseModel): Attributes: topic_summary: The new topic summary for the conversation. + """ topic_summary: str = Field( diff --git a/src/models/api/requests/feedback.py b/src/models/api/requests/feedback.py index 684333a81..387c98185 100644 --- a/src/models/api/requests/feedback.py +++ b/src/models/api/requests/feedback.py @@ -18,6 +18,7 @@ class FeedbackRequest(BaseModel): sentiment: The optional sentiment. user_feedback: The optional user feedback. categories: The optional list of positive or negative feedback categories. + """ conversation_id: str = Field( @@ -114,6 +115,7 @@ def check_uuid(cls, value: str) -> str: Raises: ValueError: If value is not a valid SUID. + """ if not suid.check_suid(value): raise ValueError(f"Improper conversation ID {value}") @@ -132,6 +134,7 @@ def check_sentiment(cls, value: Optional[int]) -> Optional[int]: Raises: ValueError: If value is not -1, 1, or None. + """ if value not in {-1, 1, None}: raise ValueError( @@ -156,6 +159,7 @@ def validate_categories( Returns: The normalized list with duplicates removed, or None. + """ if value is None: return value @@ -174,6 +178,7 @@ def check_feedback_provided(self) -> Self: Raises: ValueError: If none of sentiment, user_feedback, or categories are provided. + """ if ( self.sentiment is None @@ -192,6 +197,7 @@ class FeedbackStatusUpdateRequest(BaseModel): Attributes: status: Value of the desired feedback enabled state. + """ status: bool = Field( @@ -208,5 +214,6 @@ def get_value(self) -> bool: Returns: True if feedback is enabled, False otherwise. + """ return self.status diff --git a/src/models/api/requests/mcp_servers.py b/src/models/api/requests/mcp_servers.py index 530ddde5b..86dfb8d8a 100644 --- a/src/models/api/requests/mcp_servers.py +++ b/src/models/api/requests/mcp_servers.py @@ -17,6 +17,7 @@ class MCPServerRegistrationRequest(BaseModel): authorization_headers: Optional headers to send to the MCP server. headers: Optional list of HTTP header names to forward from incoming requests. timeout: Optional request timeout in seconds. + """ name: str = Field( @@ -113,6 +114,7 @@ def validate_url(cls, value: str) -> str: Raises: ValueError: If URL does not start with http:// or https://. + """ if not value.startswith(("http://", "https://")): raise ValueError("URL must use http:// or https:// scheme") @@ -138,6 +140,7 @@ def validate_authorization_header_values( Raises: ValueError: If any header value is not a supported keyword. + """ if value is None: return value diff --git a/src/models/api/requests/prompts.py b/src/models/api/requests/prompts.py index 9cbfb63d2..d180885d3 100644 --- a/src/models/api/requests/prompts.py +++ b/src/models/api/requests/prompts.py @@ -11,6 +11,7 @@ class PromptCreateRequest(BaseModel): Attributes: prompt: Prompt text with variable placeholders. variables: Variable names allowed in the template. + """ prompt: str = Field( @@ -46,6 +47,7 @@ class PromptUpdateRequest(BaseModel): version: Current version being updated. set_as_default: Whether the new version becomes the default. variables: Updated allowed variable names. + """ prompt: str = Field( diff --git a/src/models/api/requests/query.py b/src/models/api/requests/query.py index 8b6e7b83d..051833752 100644 --- a/src/models/api/requests/query.py +++ b/src/models/api/requests/query.py @@ -25,6 +25,7 @@ class QueryRequest(BaseModel): vector_store_ids: The optional list of specific vector store IDs to query for RAG. shield_ids: The optional list of configured shield names to apply. solr: Optional Solr inline RAG options (mode, filters) or legacy filter-only dict. + """ query: str = Field( @@ -172,6 +173,7 @@ def check_uuid(cls, value: Optional[str]) -> Optional[str]: 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}'") @@ -186,6 +188,7 @@ def validate_provider_and_model(self) -> Self: Raises: ValueError: If only provider or only model is provided (they must be set together). + """ if self.model and not self.provider: raise ValueError("Provider must be specified if model is specified") @@ -202,6 +205,7 @@ def validate_media_type(self) -> Self: Raises: ValueError: If media_type is not equal to MEDIA_TYPE_JSON or MEDIA_TYPE_TEXT. + """ if self.media_type and self.media_type not in [ MEDIA_TYPE_JSON, @@ -218,6 +222,7 @@ class StreamingInterruptRequest(BaseModel): Attributes: request_id: Unique ID of the active streaming request to interrupt. + """ request_id: str = Field( @@ -247,6 +252,7 @@ def check_request_id(cls, value: str) -> str: Raises: ValueError: If the request identifier is not a valid SUID. + """ if not suid.check_suid(value): raise ValueError(f"Improper request ID {value}") diff --git a/src/models/api/requests/responses_openai.py b/src/models/api/requests/responses_openai.py index 809b6963f..24c41cf34 100644 --- a/src/models/api/requests/responses_openai.py +++ b/src/models/api/requests/responses_openai.py @@ -60,6 +60,7 @@ class ResponsesRequest(BaseModel): shield_ids: LCORE-specific list of configured shield names to apply. If None, all configured shields are used. solr: Optional Solr inline RAG options (mode, filters) or legacy filter-only dict. + """ input: ResponseInput @@ -122,6 +123,7 @@ def validate_body_size(cls, values: Any) -> Any: Raises: ValueError: If the JSON-serialized size of ``values`` exceeds 65,536 characters. + """ try: serialized = json.dumps(values) @@ -150,6 +152,7 @@ def validate_conversation_and_previous_response_id_mutually_exclusive(self) -> S Returns: Self: The validated model instance. + """ if self.conversation and self.previous_response_id: raise ValueError( diff --git a/src/models/api/requests/rlsapi.py b/src/models/api/requests/rlsapi.py index 100cae763..330398b3c 100644 --- a/src/models/api/requests/rlsapi.py +++ b/src/models/api/requests/rlsapi.py @@ -28,6 +28,7 @@ class RlsapiV1Attachment(ConfigurationBase): Attributes: contents: The textual contents of the file read on the client machine. mimetype: The MIME type of the file. + """ contents: str = Field( @@ -48,6 +49,7 @@ class RlsapiV1Terminal(ConfigurationBase): Attributes: output: The textual contents of the terminal read on the client machine. + """ output: str = Field( @@ -66,6 +68,7 @@ class RlsapiV1SystemInfo(ConfigurationBase): version: The version of the operating system. arch: The architecture of the client machine. system_id: The id of the client machine. + """ os: str = Field( @@ -103,6 +106,7 @@ class RlsapiV1CLA(ConfigurationBase): Attributes: nevra: The NEVRA (Name-Epoch-Version-Release-Architecture) of the CLA. version: The version of the command line assistant. + """ nevra: str = Field( @@ -128,6 +132,7 @@ class RlsapiV1Context(ConfigurationBase): terminal: Terminal object received by the client. systeminfo: System information object received by the client. cla: Command Line Assistant information. + """ stdin: str = Field( @@ -173,6 +178,7 @@ class RlsapiV1InferRequest(ConfigurationBase): ), ) ``` + """ question: str = Field( @@ -214,6 +220,7 @@ def validate_question(cls, value: str) -> str: Raises: ValueError: If the question is empty or whitespace-only. + """ stripped = value.strip() if not stripped: @@ -234,6 +241,7 @@ def get_input_source(self) -> str: Returns: The combined input string with sources separated by double newlines. + """ # pylint: disable=no-member # Pydantic fields are dynamic parts = [ diff --git a/src/models/api/requests/saved_prompts.py b/src/models/api/requests/saved_prompts.py index a77ace05e..8528ac017 100644 --- a/src/models/api/requests/saved_prompts.py +++ b/src/models/api/requests/saved_prompts.py @@ -12,6 +12,7 @@ class SavedPromptCreateRequest(BaseModel): Attributes: name: Display name of the saved prompt. content: Prompt body text. + """ name: str = Field( diff --git a/src/models/api/requests/vector_stores.py b/src/models/api/requests/vector_stores.py index 3eb37a498..59f439a8b 100644 --- a/src/models/api/requests/vector_stores.py +++ b/src/models/api/requests/vector_stores.py @@ -15,6 +15,7 @@ class VectorStoreCreateRequest(BaseModel): chunking_strategy: Optional chunking strategy configuration. provider_id: Optional vector store provider identifier. metadata: Optional metadata dictionary for storing session information. + """ name: str = Field( @@ -79,6 +80,7 @@ class VectorStoreUpdateRequest(BaseModel): name: New name for the vector store. expires_at: Optional expiration timestamp. metadata: Optional metadata dictionary for storing session information. + """ name: Optional[str] = Field( @@ -124,6 +126,7 @@ def check_at_least_one_field(self) -> Self: Returns: Self: The validated model instance. + """ if self.name is None and self.expires_at is None and self.metadata is None: raise ValueError( @@ -139,6 +142,7 @@ class VectorStoreFileCreateRequest(BaseModel): file_id: ID of the file to add to the vector store. attributes: Optional metadata key-value pairs (max 16 pairs). chunking_strategy: Optional chunking strategy configuration. + """ file_id: str = Field( @@ -198,6 +202,7 @@ def validate_attributes( Returns: The validated attributes dictionary. + """ if value is None: return value diff --git a/src/models/api/responses/error/bad_request.py b/src/models/api/responses/error/bad_request.py index e1129d638..df618f2e3 100644 --- a/src/models/api/responses/error/bad_request.py +++ b/src/models/api/responses/error/bad_request.py @@ -53,6 +53,7 @@ def __init__(self, *, resource: str, resource_id: str) -> None: Args: resource: Type of the resource (for message), e.g. conversation or provider. resource_id: 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." diff --git a/src/models/api/responses/error/bases.py b/src/models/api/responses/error/bases.py index 7fbeba4bf..0cba3659e 100644 --- a/src/models/api/responses/error/bases.py +++ b/src/models/api/responses/error/bases.py @@ -19,6 +19,7 @@ class AbstractErrorResponse(BaseModel): Attributes: status_code: HTTP status code for the error response. detail: The detail model containing error summary and cause. + """ status_code: int = Field( @@ -35,6 +36,7 @@ def __init__(self, *, response: str, cause: str, status_code: int) -> None: response: A short, user-facing summary of the error. cause: A more detailed explanation of the error cause. status_code: The HTTP status code to associate with this error response. + """ super().__init__( status_code=status_code, detail=DetailModel(response=response, cause=cause) @@ -47,6 +49,7 @@ def get_description(cls) -> str: Returns: The class description attribute if present; otherwise the class docstring; if neither is present, an empty string. + """ return getattr(cls, "description", cls.__doc__ or "") @@ -70,6 +73,7 @@ def openapi_response(cls, examples: Optional[list[str]] = None) -> dict[str, Any Raises: SchemaError: If any example in the model schema lacks a label or detail field. + """ schema = cls.model_json_schema() model_examples = schema.get("examples", []) diff --git a/src/models/api/responses/error/conflict.py b/src/models/api/responses/error/conflict.py index 5a5fb9023..5c4d71568 100644 --- a/src/models/api/responses/error/conflict.py +++ b/src/models/api/responses/error/conflict.py @@ -67,6 +67,7 @@ def __init__(self, *, resource: str, resource_id: str) -> None: Args: resource: Type of the resource (e.g. MCP server). resource_id: The identifier of the conflicting resource. + """ response = f"{resource.title()} already exists" cause = f"{resource.title()} with name '{resource_id}' is already registered" @@ -83,6 +84,7 @@ def mcp_tool(cls, server_label: str) -> Self: Returns: A response with a cause describing the MCP tool conflict. + """ return cls._from_cause( response="Tool conflict", @@ -99,6 +101,7 @@ def file_search(cls) -> Self: Returns: A response with a cause describing the file_search tool conflict. + """ return cls._from_cause( response="Tool conflict", @@ -119,6 +122,7 @@ def _from_cause(cls, *, response: str, cause: str) -> Self: Returns: A new ConflictResponse instance. + """ instance = cls.__new__(cls) AbstractErrorResponse.__init__( diff --git a/src/models/api/responses/error/content_too_large.py b/src/models/api/responses/error/content_too_large.py index 6adb60213..689900547 100644 --- a/src/models/api/responses/error/content_too_large.py +++ b/src/models/api/responses/error/content_too_large.py @@ -50,6 +50,7 @@ def __init__( Args: response: Short summary of the error. Defaults to "Prompt is too long". model: The model identifier for which the prompt is too long. + """ if model: cause = f"The input exceeds the context window size of model '{model}'." @@ -108,6 +109,7 @@ def exceeds_local_limit( Returns: A response whose cause includes both sizes and the maximum size in MB (floored). + """ cause = ( f"File size {file_size} bytes exceeds maximum allowed " @@ -130,6 +132,7 @@ def from_backend_rejection( Returns: A response whose cause prefixes the message with a fixed label. + """ cause = f"File upload rejected: {message}" return cls(response=response, cause=cause) @@ -140,6 +143,7 @@ def __init__(self, *, response: str, cause: str) -> None: Args: response: Short summary of the error. cause: Detailed explanation for operators and clients. + """ super().__init__( response=response, diff --git a/src/models/api/responses/error/forbidden.py b/src/models/api/responses/error/forbidden.py index bf1c8f326..507778f06 100644 --- a/src/models/api/responses/error/forbidden.py +++ b/src/models/api/responses/error/forbidden.py @@ -121,6 +121,7 @@ def conversation(cls, action: str, resource_id: str, user_id: str) -> Self: Returns: Error response indicating the user is not permitted to perform the specified action on the conversation, with response and cause set. + """ response = "User does not have permission to perform this action" cause = ( @@ -141,6 +142,7 @@ def saved_prompt(cls, action: str, resource_id: str, user_id: str) -> Self: Returns: Error response indicating the user is not permitted to perform the specified action on the saved prompt. + """ response = "User does not have permission to perform this action" cause = ( @@ -158,6 +160,7 @@ def endpoint(cls, user_id: str) -> Self: Returns: 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." @@ -170,6 +173,7 @@ def feedback_disabled(cls) -> Self: Returns: 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", @@ -184,6 +188,7 @@ def model_override(cls) -> Self: 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=( @@ -207,6 +212,7 @@ def mcp_server_static_config(cls, server_name: str) -> Self: Returns: A ``ForbiddenResponse`` with the static-delete user message and a cause that names ``server_name`` and states it cannot be removed via the API. + """ return cls( response="Cannot delete statically configured MCP server", @@ -222,6 +228,7 @@ def __init__(self, *, response: str, cause: str) -> None: Args: response: Human-facing error message describing the forbidden action. cause: Detailed reason for the denial, for logs or diagnostics. + """ super().__init__( response=response, cause=cause, status_code=status.HTTP_403_FORBIDDEN diff --git a/src/models/api/responses/error/internal.py b/src/models/api/responses/error/internal.py index 383f29658..20e0a5199 100644 --- a/src/models/api/responses/error/internal.py +++ b/src/models/api/responses/error/internal.py @@ -101,6 +101,7 @@ def generic(cls) -> Self: Returns: InternalServerErrorResponse instance with the standard response message "Internal server error" and a cause explaining an unexpected processing error. + """ return cls( response="Internal server error", @@ -115,6 +116,7 @@ def configuration_not_loaded(cls) -> Self: Returns: InternalServerErrorResponse with response "Configuration is not loaded" and cause "Lightspeed Stack configuration has not been initialized." + """ return cls( response="Configuration is not loaded", @@ -132,6 +134,7 @@ def feedback_path_invalid(cls, path: str) -> Self: Returns: Error response with response message "Failed to store feedback" and a cause indicating the failed directory. + """ return cls( response="Failed to store feedback", @@ -149,6 +152,7 @@ def query_failed(cls, cause: str) -> Self: Returns: Error response with response "Error while processing query" and the provided cause. + """ return cls( response="Error while processing query", @@ -163,6 +167,7 @@ def cache_unavailable(cls) -> Self: Returns: Error response with a message that the conversation cache is not configured and a corresponding cause. + """ return cls( response="Conversation cache not configured", @@ -177,6 +182,7 @@ def database_error(cls) -> Self: Returns: Instance with response "Database query failed" and cause "Failed to query the database". + """ return cls( response="Database query failed", @@ -191,6 +197,7 @@ def mcp_server_registration_failed(cls) -> Self: Returns: Error response with response "Failed to register MCP server" and cause "Could not register the MCP server with the remote service." + """ return cls( response="Failed to register MCP server", @@ -203,6 +210,7 @@ def __init__(self, *, response: str, cause: str) -> None: Args: response: Public-facing error message. cause: Internal explanation of the error cause. + """ super().__init__( response=response, diff --git a/src/models/api/responses/error/not_found.py b/src/models/api/responses/error/not_found.py index e55751629..6b01a4e42 100644 --- a/src/models/api/responses/error/not_found.py +++ b/src/models/api/responses/error/not_found.py @@ -100,6 +100,7 @@ def __init__(self, *, resource: str, resource_id: Optional[str] = None) -> None: resource: Resource type that was not found (e.g. conversation, model). resource_id: Identifier of the missing resource. If None, the resource type is not configured (e.g. no model selected). + """ response = f"{resource.title()} not found" if resource_id is None: diff --git a/src/models/api/responses/error/service_unavailable.py b/src/models/api/responses/error/service_unavailable.py index 7d001d804..d95379648 100644 --- a/src/models/api/responses/error/service_unavailable.py +++ b/src/models/api/responses/error/service_unavailable.py @@ -42,6 +42,7 @@ def __init__(self, *, backend_name: str, cause: str | None = None) -> None: Args: backend_name: Name of the backend service that could not be contacted. cause: Optional detailed explanation; defaults to a generic connection error. + """ response = f"Unable to connect to {backend_name}" super().__init__( diff --git a/src/models/api/responses/error/too_many_requests.py b/src/models/api/responses/error/too_many_requests.py index 7efca8075..08343c251 100644 --- a/src/models/api/responses/error/too_many_requests.py +++ b/src/models/api/responses/error/too_many_requests.py @@ -82,6 +82,7 @@ def model(cls, model_name: str) -> Self: Returns: Response with a standard 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." @@ -96,6 +97,7 @@ def from_exception(cls, exc: QuotaExceedError) -> Self: Returns: Response with a standard quota-exceeded message and the exception text as cause. + """ response = "The quota has been exceeded" cause = str(exc) @@ -109,6 +111,7 @@ def __init__(self, *, response: str, cause: str) -> None: Args: response: Public-facing error message describing the quota condition. cause: Detailed cause stored in the error detail. + """ super().__init__( response=response, @@ -151,6 +154,7 @@ def file_upload(cls) -> Self: Returns: Response indicating the concurrent file upload limit was reached. + """ return cls( response="Too many concurrent file uploads", @@ -163,6 +167,7 @@ def vector_store_attach(cls) -> Self: Returns: Response indicating the concurrent vector store attach limit was reached. + """ return cls( response="Too many concurrent vector store file attachments", @@ -180,6 +185,7 @@ def __init__(self, *, response: str, cause: str) -> None: Args: response: Public-facing error message describing the condition. cause: Detailed cause stored in the error detail. + """ super().__init__( response=response, diff --git a/src/models/api/responses/error/unauthorized.py b/src/models/api/responses/error/unauthorized.py index 79455b11e..80dfe5ccb 100644 --- a/src/models/api/responses/error/unauthorized.py +++ b/src/models/api/responses/error/unauthorized.py @@ -91,6 +91,7 @@ def __init__(self, *, cause: str) -> None: Args: cause: Human-readable explanation (e.g. missing token, expired token). + """ response_msg = "Missing or invalid credentials provided by client" super().__init__( diff --git a/src/models/api/responses/error/unprocessable_entity.py b/src/models/api/responses/error/unprocessable_entity.py index 9088a34d5..fd4a1cf34 100644 --- a/src/models/api/responses/error/unprocessable_entity.py +++ b/src/models/api/responses/error/unprocessable_entity.py @@ -64,6 +64,7 @@ def __init__(self, *, response: str, cause: str) -> None: Args: response: Human-readable message describing what was unprocessable. cause: Specific cause or diagnostic information explaining the error. + """ super().__init__( response=response, diff --git a/src/models/api/responses/successful/bases.py b/src/models/api/responses/successful/bases.py index c4355d25b..841ecbb94 100644 --- a/src/models/api/responses/successful/bases.py +++ b/src/models/api/responses/successful/bases.py @@ -59,6 +59,7 @@ def openapi_response(cls) -> dict[str, Any]: Raises: SchemaError: If the model JSON schema has no examples list. + """ schema = cls.model_json_schema() model_examples = schema.get("examples") diff --git a/src/models/api/responses/successful/catalog.py b/src/models/api/responses/successful/catalog.py index 072472bae..f6594ba47 100644 --- a/src/models/api/responses/successful/catalog.py +++ b/src/models/api/responses/successful/catalog.py @@ -15,6 +15,7 @@ class SkillsResponse(AbstractSuccessfulResponse): Attributes: skills: List of loaded skills with metadata (name and description). + """ skills: list[SkillMetadata] = Field( diff --git a/src/models/api/responses/successful/configuration.py b/src/models/api/responses/successful/configuration.py index c97374a6b..c6ce2d091 100644 --- a/src/models/api/responses/successful/configuration.py +++ b/src/models/api/responses/successful/configuration.py @@ -11,6 +11,7 @@ class ConfigurationResponse(AbstractSuccessfulResponse): Attributes: configuration: Parsed application configuration returned to the client. + """ configuration: Configuration diff --git a/src/models/api/responses/successful/conversations.py b/src/models/api/responses/successful/conversations.py index c1bacae42..b9d1bcab4 100644 --- a/src/models/api/responses/successful/conversations.py +++ b/src/models/api/responses/successful/conversations.py @@ -24,6 +24,7 @@ class ConversationResponse(AbstractSuccessfulResponse): Attributes: conversation_id: The conversation ID (UUID). chat_history: The chat history as a list of conversation turns. + """ conversation_id: str = Field( @@ -122,6 +123,7 @@ class ConversationsListResponse(AbstractSuccessfulResponse): Attributes: conversations: List of conversation details associated with the user. + """ conversations: list[ConversationDetails] @@ -160,6 +162,7 @@ class ConversationsListResponseV2(AbstractSuccessfulResponse): Attributes: conversations: List of conversation data associated with the user. + """ conversations: list[ConversationData] @@ -188,6 +191,7 @@ class ConversationUpdateResponse(AbstractSuccessfulResponse): conversation_id: The conversation ID (UUID) that was updated. success: Whether the update was successful. message: A message about the update result. + """ conversation_id: str = Field( diff --git a/src/models/api/responses/successful/feedback.py b/src/models/api/responses/successful/feedback.py index c6bc86113..f404f8bc4 100644 --- a/src/models/api/responses/successful/feedback.py +++ b/src/models/api/responses/successful/feedback.py @@ -12,6 +12,7 @@ class FeedbackResponse(AbstractSuccessfulResponse): Attributes: response: The response of the feedback request. + """ response: str = Field( @@ -37,6 +38,7 @@ class FeedbackStatusUpdateResponse(AbstractSuccessfulResponse): Attributes: status: The previous and current status of the service and who updated it. + """ status: dict[str, Any] diff --git a/src/models/api/responses/successful/mcp_servers.py b/src/models/api/responses/successful/mcp_servers.py index 92ceeb867..c915e4b7b 100644 --- a/src/models/api/responses/successful/mcp_servers.py +++ b/src/models/api/responses/successful/mcp_servers.py @@ -16,6 +16,7 @@ class MCPClientAuthOptionsResponse(AbstractSuccessfulResponse): Attributes: servers: MCP servers that declare client authentication headers. + """ servers: list[MCPServerAuthInfo] = Field( @@ -51,6 +52,7 @@ class MCPServerRegistrationResponse(AbstractSuccessfulResponse): url: Registered MCP server URL. provider_id: MCP provider identification. message: Status message. + """ name: str = Field(..., description="Registered MCP server name") @@ -77,6 +79,7 @@ class MCPServerListResponse(AbstractSuccessfulResponse): Attributes: servers: All registered MCP servers (static and dynamic). + """ servers: list[MCPServerInfo] = Field( @@ -115,6 +118,7 @@ class MCPServerDeleteResponse(AbstractDeleteResponse): name: Name of the MCP server targeted for deletion. deleted: Whether the server was successfully deleted (True) or not found (False). response: Description of the result, e.g. "MCP server deleted successfully". + """ resource_name: ClassVar[str] = "MCP server" diff --git a/src/models/api/responses/successful/probes.py b/src/models/api/responses/successful/probes.py index 735bae232..544e993f5 100644 --- a/src/models/api/responses/successful/probes.py +++ b/src/models/api/responses/successful/probes.py @@ -18,6 +18,7 @@ class InfoResponse(AbstractSuccessfulResponse): name: Service name. service_version: Service version. ogx_version: OGX version. + """ name: str = Field( @@ -58,6 +59,7 @@ class ReadinessResponse(AbstractSuccessfulResponse): overall_status: Overall service health status (healthy/degraded/unhealthy). impacts: Optional list of functional impacts when degraded or unhealthy. providers: List of unhealthy providers (empty when all healthy). + """ ready: bool = Field( @@ -128,6 +130,7 @@ class LivenessResponse(AbstractSuccessfulResponse): Attributes: alive: If app is alive. + """ alive: bool = Field( @@ -154,6 +157,7 @@ class StatusResponse(AbstractSuccessfulResponse): Attributes: functionality: The functionality of the service. status: The status of the service. + """ functionality: str = Field( @@ -188,6 +192,7 @@ class AuthorizedResponse(AbstractSuccessfulResponse): user_id: The ID of the logged in user. username: The name of the logged in user. skip_userid_check: Whether to skip the user ID check. + """ user_id: str = Field( diff --git a/src/models/api/responses/successful/prompts.py b/src/models/api/responses/successful/prompts.py index f53145db9..5603ffeb9 100644 --- a/src/models/api/responses/successful/prompts.py +++ b/src/models/api/responses/successful/prompts.py @@ -19,6 +19,7 @@ class PromptResourceResponse(AbstractSuccessfulResponse): is_default: Whether this version is the default. prompt: Prompt text with placeholders. variables: Variable names used in the template. + """ prompt_id: str = Field(..., description="Prompt identifier from OGX") @@ -52,6 +53,7 @@ class PromptsListResponse(AbstractSuccessfulResponse): Attributes: data: Prompt entries as returned by the OGX list API. + """ data: list[PromptResourceResponse] = Field( @@ -86,6 +88,7 @@ class PromptDeleteResponse(AbstractDeleteResponse): prompt_id: Prompt identifier that was passed to delete. deleted: Whether the prompt was deleted successfully response: Human readable response + """ resource_name: ClassVar[str] = "Prompt" diff --git a/src/models/api/responses/successful/query.py b/src/models/api/responses/successful/query.py index 2bd718b74..a5d090733 100644 --- a/src/models/api/responses/successful/query.py +++ b/src/models/api/responses/successful/query.py @@ -34,6 +34,7 @@ class QueryResponse(AbstractSuccessfulResponse): input_tokens: Number of tokens sent to LLM. output_tokens: Number of tokens received from LLM. available_quotas: Quota available as measured by all configured quota limiters. + """ conversation_id: Optional[str] = Field( @@ -225,6 +226,7 @@ class StreamingInterruptResponse(AbstractSuccessfulResponse): request_id: The streaming request ID targeted by the interrupt call. interrupted: Whether an in-progress stream was interrupted. message: Human-readable interruption status message. + """ request_id: str = Field( diff --git a/src/models/api/responses/successful/responses_openai.py b/src/models/api/responses/successful/responses_openai.py index 485d9dd08..7a152e52c 100644 --- a/src/models/api/responses/successful/responses_openai.py +++ b/src/models/api/responses/successful/responses_openai.py @@ -69,6 +69,7 @@ class ResponsesResponse(AbstractSuccessfulResponse): available_quotas: Remaining token quotas for the user (LCORE-specific). output_text: Aggregated text output from all output_text items in the output array. + """ created_at: int diff --git a/src/models/api/responses/successful/rlsapi.py b/src/models/api/responses/successful/rlsapi.py index 6d85dd514..536a3862e 100644 --- a/src/models/api/responses/successful/rlsapi.py +++ b/src/models/api/responses/successful/rlsapi.py @@ -26,6 +26,7 @@ class RlsapiV1InferData(ConfigurationBase): referenced_documents: Source documents referenced (verbose mode only). input_tokens: Number of input tokens consumed (verbose mode only). output_tokens: Number of output tokens generated (verbose mode only). + """ text: str = Field( @@ -71,6 +72,7 @@ class RlsapiV1InferResponse(AbstractSuccessfulResponse): Attributes: data: Response data containing text and request_id. + """ data: RlsapiV1InferData = Field( diff --git a/src/models/api/responses/successful/saved_prompts.py b/src/models/api/responses/successful/saved_prompts.py index fff80edc4..22e32ba78 100644 --- a/src/models/api/responses/successful/saved_prompts.py +++ b/src/models/api/responses/successful/saved_prompts.py @@ -18,6 +18,7 @@ class SavedPromptsConfigResponse(AbstractSuccessfulResponse): max_prompts_per_user: Maximum number of saved prompts allowed per user. max_display_name_length: Maximum character length for prompt display name. max_content_length: Maximum character length for prompt content body. + """ max_prompts_per_user: int = Field( @@ -59,6 +60,7 @@ class SavedPromptResponse(AbstractSuccessfulResponse): content: Prompt body text. created_at: When the prompt was created. updated_at: When the prompt was last updated. + """ id: str = Field( @@ -109,6 +111,7 @@ class SavedPromptsListResponse(AbstractSuccessfulResponse): Attributes: prompts: Saved prompts ordered by created_at descending (newest first). + """ prompts: list[SavedPromptResponse] = Field( @@ -144,6 +147,7 @@ class SavedPromptDeleteResponse(AbstractDeleteResponse): prompt_id: Saved prompt identifier that was passed to delete. deleted: Whether the prompt was deleted successfully. response: Human-readable outcome of the delete operation. + """ resource_name: ClassVar[str] = "Saved prompt" diff --git a/src/models/api/responses/successful/vector_stores.py b/src/models/api/responses/successful/vector_stores.py index 74158c6af..af0d08f4c 100644 --- a/src/models/api/responses/successful/vector_stores.py +++ b/src/models/api/responses/successful/vector_stores.py @@ -23,6 +23,7 @@ class VectorStoreResponse(AbstractSuccessfulResponse): status: Vector store status. usage_bytes: Storage usage in bytes. metadata: Optional metadata dictionary for storing session information. + """ id: str = Field(..., description="Vector store ID") @@ -72,6 +73,7 @@ class VectorStoresListResponse(AbstractSuccessfulResponse): Attributes: data: List of vector store objects. object: Object type (always "list"). + """ data: list[VectorStoreResponse] = Field( @@ -191,6 +193,7 @@ class FileResponse(AbstractSuccessfulResponse): created_at: Unix timestamp when created. purpose: File purpose. object: Object type (always "file"). + """ id: str = Field(..., description="File ID") @@ -227,6 +230,7 @@ class VectorStoreFileResponse(AbstractSuccessfulResponse): attributes: Optional metadata key-value pairs. last_error: Optional error message if processing failed. object: Object type (always "vector_store.file"). + """ id: str = Field(..., description="Vector store file ID") @@ -267,6 +271,7 @@ class VectorStoreFilesListResponse(AbstractSuccessfulResponse): Attributes: data: List of vector store file objects. object: Object type (always "list"). + """ data: list[VectorStoreFileResponse] = Field( diff --git a/src/models/common/agents/stream_payloads.py b/src/models/common/agents/stream_payloads.py index aab799d7b..f0d4b601a 100644 --- a/src/models/common/agents/stream_payloads.py +++ b/src/models/common/agents/stream_payloads.py @@ -76,6 +76,7 @@ def create(cls, *, status_code: int, response: str, cause: str) -> Self: Returns: Error stream payload instance. + """ return cls( data=ErrorEventData(status_code=status_code, response=response, cause=cause) @@ -90,6 +91,7 @@ def from_error_response(cls, error_response: AbstractErrorResponse) -> Self: Returns: Error stream payload instance. + """ return cls.create( status_code=error_response.status_code, @@ -118,6 +120,7 @@ def create(cls, *, conversation_id: str, request_id: str) -> Self: Returns: Start stream payload instance. + """ return cls( data=StartEventData(conversation_id=conversation_id, request_id=request_id) @@ -139,6 +142,7 @@ def create(cls, *, request_id: str) -> Self: Returns: Interrupted stream payload instance. + """ return cls(data=InterruptedEventData(request_id=request_id)) @@ -173,6 +177,7 @@ def create( Returns: End stream payload instance. + """ return cls( data=EndEventData( @@ -218,6 +223,7 @@ def create(cls, *, chunk_id: int, token: str) -> Self: Returns: Token stream payload instance. + """ return cls(data=TokenChunkData(id=chunk_id, token=token)) @@ -242,6 +248,7 @@ def create(cls, *, chunk_id: int, token: str) -> Self: Returns: Turn-complete stream payload instance. + """ return cls(data=TokenChunkData(id=chunk_id, token=token)) diff --git a/src/models/common/agents/turn_accumulator.py b/src/models/common/agents/turn_accumulator.py index 13ee24d12..d4b73a109 100644 --- a/src/models/common/agents/turn_accumulator.py +++ b/src/models/common/agents/turn_accumulator.py @@ -24,6 +24,7 @@ class AgentTurnAccumulator: # pylint: disable=too-many-instance-attributes emitted_tool_call_ids: Tool call IDs already sent or recorded. emitted_tool_result_ids: Tool result IDs already sent or recorded. seen_docs: Referenced-document keys already added (deduplication). + """ vector_store_ids: Final[list[str]] diff --git a/src/models/common/conversation.py b/src/models/common/conversation.py index e02884a23..78ca8a25a 100644 --- a/src/models/common/conversation.py +++ b/src/models/common/conversation.py @@ -18,6 +18,7 @@ class ConversationData(BaseModel): conversation_id: The conversation ID topic_summary: The topic summary for the conversation (can be None) last_message_timestamp: The timestamp of the last message in the conversation + """ conversation_id: str @@ -49,6 +50,7 @@ class ConversationDetails(BaseModel): topic_summary="Openshift Microservices Deployment Strategies", ) ``` + """ conversation_id: str = Field( @@ -101,6 +103,7 @@ class Message(BaseModel): content: The message content. type: The type of message. referenced_documents: Optional list of documents referenced in an assistant response. + """ content: str = Field( @@ -130,6 +133,7 @@ class ConversationTurn(BaseModel): model: Model identifier used for this turn. started_at: ISO 8601 timestamp when the turn started. completed_at: ISO 8601 timestamp when the turn completed. + """ messages: list[Message] = Field( diff --git a/src/models/common/health.py b/src/models/common/health.py index ec52db2de..92718e8f8 100644 --- a/src/models/common/health.py +++ b/src/models/common/health.py @@ -42,6 +42,7 @@ class ProviderHealthStatus(BaseModel): provider_id: The ID of the provider. status: The health status ('ok', 'unhealthy', 'not_implemented'). message: Optional message about the health status. + """ provider_id: str = Field( diff --git a/src/models/common/mcp.py b/src/models/common/mcp.py index 6f671ddc0..7103fdf40 100644 --- a/src/models/common/mcp.py +++ b/src/models/common/mcp.py @@ -21,6 +21,7 @@ class MCPServerInfo(BaseModel): url: URL of the MCP server endpoint. provider_id: MCP provider identification. source: Whether the server was registered statically (config) or dynamically (api). + """ name: str = Field(..., description="MCP server name") diff --git a/src/models/common/query.py b/src/models/common/query.py index 3facfbf9c..836e18271 100644 --- a/src/models/common/query.py +++ b/src/models/common/query.py @@ -33,6 +33,7 @@ def _validate_image_magic_bytes(data: bytes, content_type: str) -> None: Raises: ValueError: If the data does not match the expected image format. + """ pattern = _IMAGE_SIGNATURES.get(content_type) if pattern and not pattern.match(data): @@ -51,6 +52,7 @@ class Attachment(BaseModel): attachment_type: The attachment type, like "log", "configuration", "image" etc. content_type: The content type as defined in MIME standard content: The actual attachment content (text or base64-encoded image data) + """ attachment_type: str = Field( @@ -77,6 +79,7 @@ def validate_image_attachment(self) -> Self: ValueError: If attachment_type and content_type are inconsistent (one indicates an image while the other does not), if image content is not valid base64, or if decoded size exceeds the limit. + """ is_image_content_type = self.content_type in IMAGE_CONTENT_TYPES is_image_attachment_type = self.attachment_type == "image" @@ -143,6 +146,7 @@ class SolrVectorSearchRequest(BaseModel): Legacy clients may send a plain JSON object with filter keys only; that object is accepted as filters with mode unset (server default applies). + """ model_config = ConfigDict(extra="forbid") @@ -202,6 +206,7 @@ def coerce_legacy_plain_dict(cls, data: Any) -> Any: Returns: Normalized dict for Pydantic model validation, or the original non-dict value. + """ if data is None or not isinstance(data, dict): return data diff --git a/src/models/common/responses/contexts.py b/src/models/common/responses/contexts.py index 98f99354c..d8c6b0ee5 100644 --- a/src/models/common/responses/contexts.py +++ b/src/models/common/responses/contexts.py @@ -90,6 +90,7 @@ class ResponseGeneratorContext: # pylint: disable=too-many-instance-attributes inline_rag_context: Inline RAG context vector_store_ids: Vector store IDs used in the query for source resolution. rag_id_mapping: Mapping from vector_db_id to user-facing rag_id. + """ # Conversation & User context diff --git a/src/models/common/responses/responses_api_params.py b/src/models/common/responses/responses_api_params.py index aa141f702..d47666abf 100644 --- a/src/models/common/responses/responses_api_params.py +++ b/src/models/common/responses/responses_api_params.py @@ -139,6 +139,7 @@ def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]: Returns: Serializable dict for the Responses API request body. + """ result = super().model_dump(*args, **kwargs) if self.previous_response_id or self.omit_conversation: @@ -152,6 +153,7 @@ def echoed_params(self, rag_id_mapping: Mapping[str, str]) -> dict[str, Any]: rag_id_mapping: OGX vector_db_id to user-facing RAG id (from app config). Returns: dict[str, Any]: Field names and values to merge into the response object. + """ data = self.model_dump(include=_ECHOED_FIELDS) if self.tools is not None: diff --git a/src/models/common/responses/responses_conversation_context.py b/src/models/common/responses/responses_conversation_context.py index f1a7bfbf0..aabb7e339 100644 --- a/src/models/common/responses/responses_conversation_context.py +++ b/src/models/common/responses/responses_conversation_context.py @@ -19,6 +19,7 @@ class ResponsesConversationContext(BaseModel): conversation: Conversation ID in OGX format to use for the request. user_conversation: Resolved user conversation record, or None for new ones. generate_topic_summary: Resolved value for request.generate_topic_summary. + """ model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/src/models/common/shields.py b/src/models/common/shields.py index 8fdcdcc2f..a3cc26db6 100644 --- a/src/models/common/shields.py +++ b/src/models/common/shields.py @@ -15,6 +15,7 @@ class CatalogShield(BaseModel): provider_id: Shield provider / type discriminator. type: Catalog entry type; always shield. config: Type-specific shield configuration. + """ name: str = Field(description="Unique, user-facing name of the shield instance") diff --git a/src/models/common/skills.py b/src/models/common/skills.py index 422c3e937..58f9fed69 100644 --- a/src/models/common/skills.py +++ b/src/models/common/skills.py @@ -9,6 +9,7 @@ class SkillMetadata(BaseModel): Attributes: name: Unique name of the skill. description: Human readable description of what the skill does. + """ name: str = Field(..., description="Unique name of the skill") diff --git a/src/models/common/turn_summary.py b/src/models/common/turn_summary.py index 65948f5fc..984721a75 100644 --- a/src/models/common/turn_summary.py +++ b/src/models/common/turn_summary.py @@ -40,6 +40,7 @@ class ReferencedDocument(BaseModel): doc_url: Url to the referenced doc. doc_title: Title of the referenced doc. document_id: Document ID for preserving identity during deduplication. + """ doc_url: Optional[AnyUrl] = Field( @@ -68,6 +69,7 @@ class RAGContext(BaseModel): context_text: Formatted RAG context string for injection into the query. rag_chunks: RAG chunks from pre-query sources (BYOK + Solr). referenced_documents: Referenced documents from pre-query sources. + """ context_text: str = Field(default="", description="Formatted context for injection") diff --git a/src/models/compaction.py b/src/models/compaction.py index 30c2b6b1a..9821e240e 100644 --- a/src/models/compaction.py +++ b/src/models/compaction.py @@ -40,6 +40,7 @@ class ConversationSummary(BaseModel): summarization LLM call (e.g., ``"openai/gpt-4o-mini"``). Preserved for audit and for diagnostics when summary quality varies between models. + """ summary_text: str = Field( diff --git a/src/models/config.py b/src/models/config.py index f80c79580..d59924edb 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -87,6 +87,7 @@ def check_tls_configuration(self) -> Self: Returns: Self: The validated model instance. + """ return self @@ -149,6 +150,7 @@ def check_cors_configuration(self) -> Self: Returns: Self: The validated configuration instance. + """ # credentials are not allowed with wildcard origins per CORS/Fetch spec. # see https://fastapi.tiangolo.com/tutorial/cors/ @@ -264,6 +266,7 @@ def check_postgres_configuration(self) -> Self: Raises: ValueError: If `port` is greater than 65535. + """ if self.port > 65535: raise ValueError("Port value should be less than 65536") @@ -297,6 +300,7 @@ def check_database_configuration(self) -> Self: 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]) @@ -323,6 +327,7 @@ def db_type(self) -> Literal["sqlite", "postgres"]: Raises: ValueError: If neither SQLite nor PostgreSQL configuration is set. + """ if self.sqlite is not None: return "sqlite" @@ -343,6 +348,7 @@ def config(self) -> SQLiteDatabaseConfiguration | PostgreSQLDatabaseConfiguratio Raises: ValueError: If neither `sqlite` nor `postgres` is configured. + """ if self.sqlite is not None: return self.sqlite @@ -470,6 +476,7 @@ def validate_root_path(cls, value: str) -> str: ------ ValueError: If root_path is missing a leading slash or has a trailing slash. + """ if value and not value.startswith("/"): raise ValueError("root_path must start with '/'") @@ -498,6 +505,7 @@ def check_service_configuration(self) -> Self: Returns: self: The validated model instance. + """ if self.port > 65535: raise ValueError("Port value should be less than 65536") @@ -510,6 +518,7 @@ class ApprovalFilter(ConfigurationBase): Attributes: always: Tool names that always require human approval before execution. never: Tool names that never require approval (pre-approved). + """ always: list[str] = Field( @@ -532,6 +541,7 @@ def validate_no_overlap(self) -> Self: Returns: Self: The validated model instance. + """ overlap = set(self.always) & set(self.never) if overlap: @@ -549,6 +559,7 @@ class ApprovalsConfiguration(ConfigurationBase): before expiring. approval_retention_days: How long to retain decided approvals for audit purposes before cleanup. + """ approval_timeout_seconds: PositiveInt = Field( @@ -682,6 +693,7 @@ def resolve_auth_headers(self) -> Self: Returns: Self: The model instance with resolved_authorization_headers populated. + """ if self.authorization_headers: self._resolved_authorization_headers = resolve_authorization_headers( @@ -715,6 +727,7 @@ class UnifiedInferenceProvider(ConfigurationBase): extra: Additional provider-config keys merged verbatim into the synthesized provider's `config` block — an escape hatch for provider-specific knobs not modeled here. + """ type: Literal[ @@ -779,6 +792,7 @@ def validate_id(cls, value: Optional[str]) -> Optional[str]: ValueError: If the value is empty after stripping or contains characters other than lowercase letters, digits, underscores, and hyphens. + """ if value is None: return None @@ -820,6 +834,7 @@ class UnifiedOgxConfig(ConfigurationBase): `registered_resources.models` is a list, so a native_override that sets it replaces — not merges with — any LLM models LCORE auto-registered from `inference.providers[].allowed_models`. + """ baseline: Literal["default", "empty", "byo-llm"] = Field( @@ -966,6 +981,7 @@ def check_ogx_model(self) -> Self: Raises: ValueError: If no URL is provided and library-client mode is unspecified or disabled. + """ if self.url is None: # when URL is not set, it is supposed that OGX should be run in library mode @@ -1062,6 +1078,7 @@ def check_splunk_configuration(self) -> Self: Raises: ValueError: If enabled is True but required fields are missing. + """ if self.enabled: missing_fields = [] @@ -1121,6 +1138,7 @@ def check_storage_location_is_set_when_needed(self) -> Self: Returns: self: The validated UserDataCollection instance. + """ if self.feedback_enabled: if self.feedback_storage is None: @@ -1205,6 +1223,7 @@ def check_jsonpath(self) -> Self: ValueError: If the `jsonpath` cannot be parsed; the message includes the offending expression and parser error. + """ try: # try to parse the JSONPath @@ -1226,6 +1245,7 @@ def check_roles(self) -> Self: 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") @@ -1252,6 +1272,7 @@ def check_regex_pattern(self) -> Self: Returns: Self: The same JwtRoleRule instance. + """ if self.operator == JsonPathOperator.MATCH: if not isinstance(self.value, str): @@ -1275,6 +1296,7 @@ def compiled_regex(self) -> Optional[Pattern[str]]: 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) @@ -1560,6 +1582,7 @@ def check_authentication_model(self) -> Self: 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) @@ -1612,6 +1635,7 @@ def jwk_configuration(self) -> JwkConfiguration: Returns: JwkConfiguration: The configured JWK settings. + """ if self.module != constants.AUTH_MOD_JWK_TOKEN: raise ValueError( @@ -1632,6 +1656,7 @@ def rh_identity_configuration(self) -> RHIdentityConfiguration: 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( @@ -1662,6 +1687,7 @@ def trusted_proxy_configuration(self) -> TrustedProxyConfiguration: Raises: ValueError: If the active authentication module is not trusted-proxy. ValueError: If the trusted-proxy configuration is missing. + """ if self.module != constants.AUTH_MOD_TRUSTED_PROXY: raise ValueError( @@ -1707,6 +1733,7 @@ def get_prompts(self) -> dict[str, str]: Returns: dict[str, str]: Mapping from prompt names to prompt text. + """ return self.prompts @@ -1735,6 +1762,7 @@ def check_customization_model(self) -> Self: 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) @@ -1863,6 +1891,7 @@ def check_default_model_and_provider(self) -> Self: Returns: self (Self): The validated configuration instance. + """ if self.default_model is None and self.default_provider is not None: raise ValueError( @@ -1890,6 +1919,7 @@ def check_unique_provider_ids(self) -> Self: Returns: self (Self): The validated configuration instance. + """ seen: set[str] = set() for provider in self.providers: @@ -1929,6 +1959,7 @@ class CompactionConfiguration(ConfigurationBase): buffer_max_ratio: Hard cap on the fraction of the context window the buffer zone may occupy, regardless of buffer_turns. + """ enabled: bool = Field( @@ -2021,6 +2052,7 @@ def check_cache_configuration(self) -> Self: Returns: The validated model instance. + """ # if any backend config is provided, type must be explicitly selected if self.type is None: @@ -2066,6 +2098,7 @@ class A2AStateConfiguration(ConfigurationBase): Attributes: sqlite: SQLite database configuration for A2A state storage. postgres: PostgreSQL database configuration for A2A state storage. + """ sqlite: Optional[SQLiteDatabaseConfiguration] = Field( @@ -2307,6 +2340,7 @@ class VectorStoreProviderBase(ConfigurationBase): created against this provider. embedding_dimension: Dimensionality of embedding vectors for this provider. + """ id: str = Field( @@ -2345,6 +2379,7 @@ def validate_id(cls, value: str) -> str: Raises: ValueError: If the id is empty, uses the reserved ``byok_`` prefix, or contains characters outside ``[a-z0-9_-]``. + """ stripped = value.strip() if not stripped: @@ -2409,6 +2444,7 @@ class VectorStoreConfiguration(ConfigurationBase): providers: Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora). + """ default_provider: Optional[str] = Field( @@ -2446,6 +2482,7 @@ def validate_providers_and_default(self) -> Self: ValueError: If ids are duplicated, default_provider is missing for a non-empty list, is set for an empty list, or does not match a provider id. + """ # pylint: disable=no-member if not self.providers: @@ -2620,6 +2657,7 @@ def check_limiter_periods(self) -> Self: Raises: ValueError: If SQLite is configured and it can not use the period of a limiter. + """ if self.sqlite is None: return self @@ -2898,6 +2936,7 @@ class SavedPromptsConfiguration(ConfigurationBase): max_prompts_per_user: Maximum number of saved prompts allowed per user. max_display_name_length: Maximum character length for the prompt display name. max_content_length: Maximum character length for the prompt content body. + """ max_prompts_per_user: PositiveInt = Field( @@ -2955,6 +2994,7 @@ class RedactionRule(ConfigurationBase): case_sensitive: Per-rule override for case sensitivity. When None, the global ``RedactionConfig.case_sensitive`` flag applies. + """ pattern: str = Field( @@ -2987,6 +3027,7 @@ class RedactionConfig(ConfigurationBase): rules: Ordered list of redaction rules applied sequentially. case_sensitive: When False, patterns are compiled with ``re.IGNORECASE``. Defaults to False. + """ rules: list[RedactionRule] = Field( @@ -3015,6 +3056,7 @@ def compile_patterns(self) -> Self: Returns: The validated configuration instance. + """ global_case_sensitive = self.case_sensitive compiled: CompiledPatterns = [] @@ -3050,6 +3092,7 @@ class ObservabilityConfiguration(ConfigurationBase): Attributes: otel: Dictionary of OTEL_* environment variables with secrets redacted. + """ otel: dict[str, str] = Field( @@ -3072,6 +3115,7 @@ def redact_secrets(cls, value: dict[str, str]) -> dict[str, str]: Returns: New dictionary with sensitive values redacted + """ # Let Pydantic handle type validation for non-dict inputs if not isinstance(value, dict): @@ -3120,6 +3164,7 @@ def _redact_header_values(header_string: str) -> str: Returns: Header string with values redacted but keys preserved + """ if not header_string or "=" not in header_string: # No key=value pairs found, redact entire string @@ -3146,6 +3191,7 @@ def from_environment(cls) -> "ObservabilityConfiguration": Returns: ObservabilityConfiguration with otel dict populated from environment. + """ otel_vars = {} for key, value in os.environ.items(): @@ -3161,6 +3207,7 @@ class QuestionValidityShieldConfiguration(ConfigurationBase): name: Unique, user-facing name identifying this shield instance. provider_id: Discriminator identifying this as a question-validity shield. config: Question-validity-specific configuration. + """ name: str = Field( @@ -3189,6 +3236,7 @@ class RedactionShieldConfiguration(ConfigurationBase): name: Unique, user-facing name identifying this shield instance. provider_id: Discriminator identifying this as a redaction shield. config: Redaction-specific configuration. + """ name: str = Field( @@ -3234,6 +3282,7 @@ class RiskDefinition(ConfigurationBase): To enable think mode (detailed reasoning) for a risk, add the risk name to the `thinking_enabled` list in `ModerationConfig`. Do not set `enable_thinking` directly - it is managed internally. + """ name: str = Field( @@ -3363,6 +3412,7 @@ def validate_api_key_requires_https(self) -> Self: Returns: The validated configuration instance. + """ # pylint: disable=no-member if self.api_key is not None and not self.url.startswith("https://"): @@ -3379,6 +3429,7 @@ class GraniteGuardianShieldConfiguration(ConfigurationBase): name: Unique, user-facing name identifying this shield instance. provider_id: Discriminator identifying this as a granite-guardian shield. config: Granite-guardian-specific configuration. + """ name: str = Field( @@ -3427,6 +3478,7 @@ def accept_llama_stack_section_alias(cls, data: Any) -> Any: Returns: Normalized mapping with the OGX section under ``ogx``. + """ if not isinstance(data, dict): return data @@ -3659,6 +3711,7 @@ def validate_shield_names_unique(self) -> Self: Raises: ValueError: If two or more shields share the same name. + """ names = [shield.name for shield in self.shields] duplicates = {name for name in names if names.count(name) > 1} @@ -3683,6 +3736,7 @@ def validate_mcp_auth_headers(self) -> Self: Returns: Self: The model instance after validation. + """ # Get authentication module value auth_module = getattr(self.authentication, "module", None) @@ -3739,6 +3793,7 @@ def validate_rlsapi_v1_quota_configuration(self) -> Self: Raises: ValueError: If quota_subject requires rh-identity but a different authentication module is configured. + """ quota_subject = self.rlsapi_v1.quota_subject # pylint: disable=no-member if quota_subject is None: @@ -3800,6 +3855,7 @@ def check_unified_vs_legacy(self) -> Self: ``library_client_config_path`` are set together, if library mode has no run source at all, or if ``config_format_version`` contradicts the detected shape. + """ # pylint: disable=no-member synthesis_input = ( @@ -3850,6 +3906,7 @@ def dump(self, filename: str | Path = "configuration.json") -> None: Parameters: ---------- filename (str | Path): 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)) diff --git a/src/observability/formats/responses.py b/src/observability/formats/responses.py index 380cfd8eb..c97fe1b1b 100644 --- a/src/observability/formats/responses.py +++ b/src/observability/formats/responses.py @@ -35,6 +35,7 @@ def build_responses_event(data: ResponsesEventData) -> dict[str, Any]: Returns: A dictionary matching the responses Splunk event format. + """ return { "input_text": data.input_text, diff --git a/src/observability/formats/rlsapi.py b/src/observability/formats/rlsapi.py index 67f7065f8..8d50eae78 100644 --- a/src/observability/formats/rlsapi.py +++ b/src/observability/formats/rlsapi.py @@ -38,6 +38,7 @@ def build_inference_event(data: InferenceEventData) -> dict[str, Any]: Returns: A dictionary matching the rlsapi Splunk event format. + """ return { "question": data.question, diff --git a/src/observability/responses_telemetry.py b/src/observability/responses_telemetry.py index 71d7c1993..d6e94f4bf 100644 --- a/src/observability/responses_telemetry.py +++ b/src/observability/responses_telemetry.py @@ -54,6 +54,7 @@ def queue_responses_splunk_event( # pylint: disable=too-many-arguments,too-many of background_tasks. Use for error paths where an HTTPException follows, since FastAPI discards BackgroundTasks on non-2xx responses. user_agent: Sanitized User-Agent string from the request header, or None. + """ if not fire_and_forget and background_tasks is None: return @@ -89,6 +90,7 @@ def queue_responses_error_event( error: The backend exception being converted into an HTTP error. api_params: Responses API parameters for the failed request. context: Request-scoped Responses API context. + """ queue_responses_splunk_event( background_tasks=context.background_tasks, @@ -115,6 +117,7 @@ def queue_blocked_response_event( api_params: Responses API parameters for the blocked request. context: Request-scoped Responses API context. response_text: Refusal text sent to the client. + """ queue_responses_splunk_event( background_tasks=context.background_tasks, @@ -144,6 +147,7 @@ def queue_completed_response_event( turn_summary: Summary containing token usage for telemetry. completed_at: Time when response handling completed. response_text: Final text sent to the client. + """ if context.moderation_result.decision != "passed": return diff --git a/src/observability/sentry.py b/src/observability/sentry.py index e8040b54c..5d5b97d23 100644 --- a/src/observability/sentry.py +++ b/src/observability/sentry.py @@ -37,6 +37,7 @@ def sentry_traces_sampler(tracing_context: dict) -> float: ------- float: 0.0 for excluded routes (no sampling), or SENTRY_DEFAULT_TRACES_SAMPLE_RATE for all other routes. + """ asgi_scope = tracing_context.get("asgi_scope", {}) path = asgi_scope.get("path") if isinstance(asgi_scope, dict) else None @@ -73,6 +74,7 @@ def initialize_sentry() -> None: Returns: ------- None + """ dsn = os.environ.get(SENTRY_DSN_ENV_VAR) diff --git a/src/observability/splunk.py b/src/observability/splunk.py index 2763b0ef1..37f717941 100644 --- a/src/observability/splunk.py +++ b/src/observability/splunk.py @@ -40,6 +40,7 @@ async def send_splunk_event(event: dict[str, Any], sourcetype: str) -> None: Args: event: The event payload to send. sourcetype: The Splunk sourcetype (e.g., "infer_with_llm", "infer_error"). + """ splunk_config = configuration.splunk if splunk_config is None or not splunk_config.enabled: @@ -138,6 +139,7 @@ def dispatch_splunk_event( background_tasks: FastAPI background task manager, or None. fire_and_forget: When True, dispatch via ``asyncio.create_task()`` instead of ``background_tasks``. + """ if fire_and_forget: task = asyncio.create_task(send_splunk_event(event, sourcetype)) diff --git a/src/ogx_configuration.py b/src/ogx_configuration.py index a6303eb0d..99f3caab6 100644 --- a/src/ogx_configuration.py +++ b/src/ogx_configuration.py @@ -45,6 +45,7 @@ def ogx_config_section(lcs_config: dict[str, Any]) -> dict[str, Any]: Returns: The OGX configuration mapping, or an empty dict when absent. + """ ogx = lcs_config.get("ogx") if isinstance(ogx, dict): @@ -129,6 +130,7 @@ def _resolve_rag_type(brag: dict[str, Any]) -> str: Returns: str: The fully-qualified OGX provider type (e.g. ``"inline::faiss"``, ``"remote::pgvector"``). + """ backend = brag.get("backend", constants.DEFAULT_RAG_BACKEND) return BACKEND_TO_PROVIDER_TYPE.get(backend, f"inline::{backend}") @@ -149,6 +151,7 @@ def increase_indent(self, flow: bool = False, indentless: bool = False) -> None: to the base implementation. indentless (bool): Ignored — this implementation always enforces indented block style. + """ _ = indentless return super().increase_indent(flow, False) @@ -175,6 +178,7 @@ def enrich_azure_entra_id_inference( Returns: None: The configuration is modified in place. + """ if azure_entra_id is None: return @@ -246,6 +250,7 @@ def construct_storage_backends_section( Returns: ------- dict[str, Any]: The storage.backends dict with new backends added. + """ output: dict[str, Any] = {} @@ -300,6 +305,7 @@ def construct_vector_stores_section( - `embedding_model`: registered OGX model id (``sentence-transformers/byok__embedding``), not the load path - `embedding_dimension`: embedding vector dimensionality + """ output = [] @@ -361,6 +367,7 @@ def construct_models_section( Returns: ------- list[dict[str, Any]]: The models list with embedding models added. + """ output: list[dict[str, Any]] = [] @@ -426,6 +433,7 @@ def _build_vector_io_config( Raises: ValueError: If ``rag_type`` is not present in VECTOR_IO_TEMPLATES. + """ template = VECTOR_IO_TEMPLATES.get(rag_type) if template is None: @@ -476,6 +484,7 @@ def construct_vector_io_providers_section( Each appended entry has `provider_id` set to "byok_", `provider_type` set from the RAG item, and a `config` with `persistence` referencing the corresponding backend. + """ output: list[dict[str, Any]] = [] @@ -526,6 +535,7 @@ def enrich_byok_rag(ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]]) Args: ogx_config: OGX configuration dict (modified in place) byok_rag: List of BYOK RAG configurations + """ if len(byok_rag) == 0: logger.info("BYOK RAG is not configured: skipping") @@ -577,6 +587,7 @@ def _vector_store_provider_by_id( Returns: Matching provider dict, or None when unset / not found. + """ if provider_id is None: return None @@ -608,6 +619,7 @@ def _upsert_vsprov_embedding_model( embedding_model: Configured embedding model path or id. embedding_dimension: Embedding vector dimensionality (required on validated ``vector_store.providers`` entries). + """ models = ogx_config.setdefault("registered_resources", {}).setdefault("models", []) model_id = f"vsprov_{provider_id}_embedding" @@ -642,6 +654,7 @@ def _vsprov_fields_and_backend( Raises: ValueError: If ``product_type`` is not a supported vector-store provider type. + """ backend_name = f"vsprov_{provider_id}_storage" if product_type == "faiss": @@ -679,6 +692,7 @@ def _replace_or_append_vector_io( vector_io: Mutable providers.vector_io list. existing_ids: Set of provider_ids already present (updated on append). provider_entry: New provider entry to install. + """ provider_id = provider_entry["provider_id"] if provider_id not in existing_ids: @@ -705,6 +719,7 @@ def _apply_vector_stores_defaults( Parameters: ogx_config: OGX configuration modified in place. designated: Provider entry selected by ``vector_store.default_provider``. + """ vector_stores = ogx_config.get("vector_stores") if not isinstance(vector_stores, dict): @@ -736,6 +751,7 @@ def _enrich_one_vector_store_provider( vector_io: ``providers.vector_io`` list (modified in place). existing_ids: Known ``provider_id`` values already in ``vector_io``. ogx_config: Full OGX config (for embedding model registration). + """ provider_id = str(entry["id"]).strip() product_type = entry["type"] @@ -785,6 +801,7 @@ def enrich_vector_store( ogx_config: OGX configuration dictionary (modified in place). vector_store: High-level ``vector_store`` section (``default_provider`` + ``providers``) as a dict. + """ vector_store = vector_store or {} providers = vector_store.get("providers") or [] @@ -841,6 +858,7 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements,too-many okp_config: OKP configuration dict. Used keys: - chunk_filter_query (str): Solr filter query for chunk retrieval - rhokp_url (str): OKP/Solr base URL (e.g. from ${env.RH_SERVER_OKP}) + """ inline_ids = rag_config.get("inline") or [] tool_ids = rag_config.get("tool") or [] @@ -1025,6 +1043,7 @@ def load_default_baseline() -> dict[str, Any]: Raises: OSError: If the shipped baseline file cannot be read. yaml.YAMLError: If the baseline file is not valid YAML. + """ with open(DEFAULT_BASELINE_RESOURCE, encoding="utf-8") as file: return yaml.safe_load(file) @@ -1045,6 +1064,7 @@ def deep_merge_list_replace( Returns: dict[str, Any]: A new merged mapping. + """ result = copy.deepcopy(base) for key, overlay_value in overlay.items(): @@ -1069,6 +1089,7 @@ def _matchable_provider_id(provider_id: Any) -> Any: Returns: ``openai`` when ``provider_id`` is the baseline conditional openai ref, otherwise ``provider_id`` unchanged. + """ if provider_id == CONDITIONAL_OPENAI_PROVIDER_ID: return "openai" @@ -1084,6 +1105,7 @@ def _strip_default_openai_inference(ogx_config: dict[str, Any]) -> None: Returns: None: ``ogx_config`` is modified in place. + """ providers = ogx_config.get("providers") if not isinstance(providers, dict): @@ -1111,6 +1133,7 @@ def _replace_or_append_inference_provider( Parameters: inference_list: Mutable providers.inference list. entry: New provider entry to install. + """ provider_id = entry["provider_id"] for index, existing in enumerate(inference_list): @@ -1141,6 +1164,7 @@ def _build_inference_entry( Returns: tuple[dict[str, Any], list[str]]: The provider entry, and its ``allowed_models`` (empty list when unset). + """ entry: dict[str, Any] = { "provider_id": emitted_id, @@ -1187,6 +1211,7 @@ def _register_high_level_models( Returns: New model resource dicts to append; empty when all are already known. + """ known = { (m.get("provider_id"), m.get("model_id")) @@ -1223,6 +1248,7 @@ def _emitted_provider_id(provider: dict[str, Any]) -> str: Returns: The provider_id to emit. + """ explicit_id = (provider.get("id") or "").strip() return explicit_id or provider["type"].replace("_", "-") @@ -1268,6 +1294,7 @@ def apply_high_level_inference( Returns: None: ``ogx_config`` is modified in place. + """ providers = inference.get("providers") or [] if not providers: @@ -1346,6 +1373,7 @@ def ensure_mcp_tool_runtime(ogx_config: dict[str, Any]) -> None: Returns: None: ``ogx_config`` is modified in place. + """ apis = ogx_config.setdefault("apis", []) if "tool_runtime" not in apis: @@ -1386,6 +1414,7 @@ def _resolve_profile_path(profile: str, config_file_dir: Optional[str]) -> Path: Returns: Path: The resolved profile path. + """ path = Path(profile) if not path.is_absolute() and config_file_dir is not None: @@ -1418,6 +1447,7 @@ def synthesize_configuration( # pylint: disable=too-many-locals Returns: dict[str, Any]: The synthesized OGX configuration. + """ unified = ogx_config_section(lcs_config).get("config") @@ -1533,6 +1563,7 @@ def synthesize_to_file( Returns: None. + """ ogx_config = synthesize_configuration(lcs_config, config_file_dir, default_baseline) @@ -1590,6 +1621,7 @@ def migrate_config_dumb( yaml.YAMLError: If an input file is not valid YAML. ValueError: If either input file does not parse to a mapping (e.g. an empty or comment-only file). + """ with open(run_yaml_path, encoding="utf-8") as file: run_yaml = yaml.safe_load(file) @@ -1646,6 +1678,7 @@ def generate_configuration( input_file: Path to input OGX config output_file: Path to write enriched config config: Lightspeed config dict (from YAML) + """ logger.info("Reading OGX configuration from file %s", input_file) @@ -1698,6 +1731,7 @@ def has_synthesis_input(lcs_config: dict[str, Any]) -> bool: Returns: bool: True when any synthesis input is present. + """ inference = lcs_config.get("inference") or {} vector_store = lcs_config.get("vector_store") or {} diff --git a/src/pydantic_ai_lightspeed/capabilities/granite_guardian/_capability.py b/src/pydantic_ai_lightspeed/capabilities/granite_guardian/_capability.py index daf0653df..032a6561b 100644 --- a/src/pydantic_ai_lightspeed/capabilities/granite_guardian/_capability.py +++ b/src/pydantic_ai_lightspeed/capabilities/granite_guardian/_capability.py @@ -109,6 +109,7 @@ def __init__( ``ToolReturnPart`` between the tool call and the rejection message; currently always empty, since a denied function tool call's result is never preserved either. + """ super().__init__(message) self.response_parts: tuple[ModelResponsePart, ...] = tuple(response_parts) @@ -133,6 +134,7 @@ async def _drain_remaining(stream: AsyncIterable[AgentStreamEvent]) -> None: Parameters: stream: The stream to exhaust. + """ try: async for _ in stream: @@ -158,6 +160,7 @@ async def _package_risk_check_task( Returns: A tuple of (model_response, threshold, violation_message). + """ name, block, threshold, violation_message = guardrail start = asyncio.get_event_loop().time() @@ -198,6 +201,7 @@ async def _run_risk_check( Raises: UnexpectedModelBehavior: When the model response is missing provider_details or logprobs. + """ token_usage = RequestUsage() @@ -244,6 +248,7 @@ def _is_mcp_list_tools_call(call_part: NativeToolCallPart) -> bool: Returns: True if this call is an MCP ``list_tools`` discovery call. + """ if not call_part.tool_name.startswith(_MCP_SERVER_TOOL_PREFIX): return False @@ -269,6 +274,7 @@ def _deny_native_results_from( Returns: Flattened call/return parts, ready to use as ``response_parts`` on a ``_ToolGuardrailViolation``. + """ response_parts: list[ModelResponsePart] = [] for index, (call_part, return_part) in enumerate(native_pairs): @@ -298,6 +304,7 @@ def _filter_guardrails( Returns: A list of guardrail tuples for enabled risks matching the point. + """ return [ ( @@ -321,6 +328,7 @@ def _get_batch_size(parallel: StrictBool | int, num_guardrail: int) -> int: Returns: The number of risk checks to run concurrently per batch. + """ if isinstance(parallel, bool): return max(1, num_guardrail) if parallel else 1 @@ -361,6 +369,7 @@ class GraniteGuardian(AbstractSafetyCapability): config: Granite Guardian configuration with risks and connection details. run_moderation_guardrail_point: The guardrail point used by the standalone ``run`` method. + """ config: GraniteGuardianConfig @@ -422,6 +431,7 @@ async def wrap_run( Returns: The agent run result, either a rejection or the handler's result. + """ user_prompt = message_to_str(ctx.prompt) @@ -489,6 +499,7 @@ async def _reject( An ``AgentRunResult`` whose output is the violation message, with ``tool_calls``/``tool_results`` still reflecting any preserved tool activity. + """ user_prompt = message_to_str(ctx.prompt) messages: list[ModelRequest | ModelResponse] = [ @@ -577,6 +588,7 @@ async def wrap_run_event_stream( Raises: _OutputGuardrailViolation: When an OUTPUT-point risk is violated. + """ _ = ctx output_guardrails = _filter_guardrails(self.config.risks, "output") @@ -693,6 +705,7 @@ async def after_tool_execute( Raises: _ToolGuardrailViolation: When a TOOL-point risk is violated. + """ _ = ctx, tool_def, args tool_guardrails = _filter_guardrails(self.config.risks, "tool") @@ -768,6 +781,7 @@ async def after_model_request( (and therefore the rejected turn's) usage totals. ``response`` is already the final, non-streaming result at this point, so its usage is folded in explicitly before raising. + """ _ = request_context tool_guardrails = _filter_guardrails(self.config.risks, "tool") @@ -811,6 +825,7 @@ async def run(self, input_text: str) -> ShieldModerationResult: Returns: A blocked result with the violation message, or a passed result. + """ filtered_guardrails = _filter_guardrails( self.config.risks, self.run_moderation_guardrail_point diff --git a/src/pydantic_ai_lightspeed/capabilities/granite_guardian/utils.py b/src/pydantic_ai_lightspeed/capabilities/granite_guardian/utils.py index bad8e2629..a5d796851 100644 --- a/src/pydantic_ai_lightspeed/capabilities/granite_guardian/utils.py +++ b/src/pydantic_ai_lightspeed/capabilities/granite_guardian/utils.py @@ -80,6 +80,7 @@ def event_text(event: AgentStreamEvent) -> str: Returns: The text contributed by this event, or an empty string. + """ if isinstance(event, PartStartEvent) and isinstance(event.part, TextPart): return event.part.content @@ -102,6 +103,7 @@ def tool_result_to_str(result: Any) -> str: Returns: A text representation of the result. + """ if isinstance(result, str): return result @@ -123,6 +125,7 @@ async def aclose_if_supported(stream: AsyncIterable[AgentStreamEvent]) -> None: Parameters: stream: The stream to close, if it supports closing. + """ aclose = getattr(stream, "aclose", None) if aclose is not None: @@ -149,6 +152,7 @@ def _search_tag(tag: str, buffer: str) -> tuple[str, bool]: Returns: A tuple of (remaining_buffer, found). The buffer is cleared when the content is not tag-like or when the tag is found. + """ if not buffer.strip().startswith("<"): return "", False @@ -167,6 +171,7 @@ def _clean_up_candidates(candidates: list[ChatCompletionTokenLogprob]) -> None: Parameters: candidates: Mutable list of token logprobs to clean in place. + """ buffer = "" while len(candidates) != 0: @@ -193,6 +198,7 @@ def _extract_tokens_inside_score_tag( Raises: UnexpectedModelBehavior: When the model output doesn't contain the expected tag structure, or the score tag has zero or multiple tokens. + """ cur_buffer = "" cur_status = ParsingStatus.BEFORE_THINK @@ -257,6 +263,7 @@ def _get_risky_probabilities(token: ChatCompletionTokenLogprob) -> float: Raises: UnexpectedModelBehavior: When neither safe nor risky tokens appear in the top logprobs (underflow). + """ safe_prob, risky_prob = 0.0, 0.0 for candidate_token in token.top_logprobs: @@ -283,6 +290,7 @@ def is_safe(threshold: float, logprobs: list[dict[str, Any]]) -> bool: Returns: True if the risky probability is below the threshold (safe). + """ token_for_confidence_threshold = _extract_tokens_inside_score_tag(logprobs) p_risky = _get_risky_probabilities(token_for_confidence_threshold) diff --git a/src/pydantic_ai_lightspeed/capabilities/question_validity/_capability.py b/src/pydantic_ai_lightspeed/capabilities/question_validity/_capability.py index 2f6a0f754..190d5f18d 100644 --- a/src/pydantic_ai_lightspeed/capabilities/question_validity/_capability.py +++ b/src/pydantic_ai_lightspeed/capabilities/question_validity/_capability.py @@ -66,6 +66,7 @@ class QuestionValidity(AbstractSafetyCapability): model = OpenAIResponsesModel("gpt-4o-mini") agent = Agent("openai:gpt-4.1", capabilities=[QuestionValidity(model)]) ``` + """ config: QuestionValidityConfig @@ -89,6 +90,7 @@ def _build_prompt(self, message: Optional[str | Sequence[UserContent]]) -> str: Returns: The rendered prompt string ready to send to the validity model. + """ return Template(self.config.model_prompt).substitute( message=message_to_str(message), @@ -112,6 +114,7 @@ async def wrap_run( Returns: The agent run result, either from the main agent or a rejection. + """ prompt = self._build_prompt(ctx.prompt) diff --git a/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py b/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py index 15681d8b4..f9971aea9 100644 --- a/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py +++ b/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py @@ -43,6 +43,7 @@ def _redact_string_content( Returns: The redacted string if redaction occurred, None otherwise. + """ result = redact_text(text, compiled_patterns) if result.redacted: @@ -61,6 +62,7 @@ def _redact_text_content( Returns: A new TextContent with redacted content if changed, None otherwise. + """ redacted_text = _redact_string_content(item.content, compiled_patterns) if redacted_text is not None: @@ -79,6 +81,7 @@ def _redact_content_item( Returns: A tuple of (redacted_item, changed_flag). + """ if isinstance(item, TextContent): redacted = _redact_text_content(item, compiled_patterns) @@ -106,6 +109,7 @@ def _redact_content_list( Returns: A new list with redacted items if any changed, None otherwise. + """ new_items: list[UserContent] = [] any_changed = False @@ -135,6 +139,7 @@ def _redact_user_prompt_part( Returns: A new UserPromptPart with redacted content, or the original. + """ if isinstance(part.content, str): redacted_text = _redact_string_content(part.content, compiled_patterns) @@ -159,6 +164,7 @@ def _redact_message_parts( Returns: A new list with redacted parts if any changed, None otherwise. + """ new_parts: list[ModelRequestPart] = [] any_changed = False @@ -187,6 +193,7 @@ def _redact_model_request( Returns: A new ModelRequest with redacted parts if changed, None otherwise. + """ redacted_parts = _redact_message_parts(message.parts, compiled_patterns) if redacted_parts is not None: @@ -208,6 +215,7 @@ def _redact_messages( Returns: A new list with redacted messages, or the original list. + """ new_messages: list[ModelMessage] = [] any_changed = False @@ -242,6 +250,7 @@ def _redact_response( Returns: A new ModelResponse with redacted content, or the original. + """ changed = False new_parts: list[ModelResponsePart] = [] @@ -276,6 +285,7 @@ class PiiRedactionCapability(AbstractSafetyCapability): Attributes: config: Redaction configuration with compiled regex rules. + """ config: RedactionConfig @@ -294,6 +304,7 @@ async def before_model_request( Returns: A new ModelRequestContext with redacted messages, or the original if no redaction occurred. + """ new_messages = _redact_messages( request_context.messages, @@ -320,6 +331,7 @@ async def after_model_request( Returns: A new ModelResponse with redacted text parts, or the original if no redaction occurred. + """ return _redact_response( response, diff --git a/src/pydantic_ai_lightspeed/capabilities/redaction/core.py b/src/pydantic_ai_lightspeed/capabilities/redaction/core.py index 4df73c303..be70fc02f 100644 --- a/src/pydantic_ai_lightspeed/capabilities/redaction/core.py +++ b/src/pydantic_ai_lightspeed/capabilities/redaction/core.py @@ -12,6 +12,7 @@ class RedactionResult(BaseModel): content: The text after all redaction rules have been applied. redacted: True if at least one rule matched and changed the text. redaction_count: Total number of substitutions made across all rules. + """ model_config = ConfigDict(frozen=True) @@ -38,6 +39,7 @@ def redact_text( A RedactionResult with the redacted content, a flag indicating whether any substitution occurred, and the total substitution count. + """ result = content total_count = 0 diff --git a/src/pydantic_ai_lightspeed/capabilities/utils.py b/src/pydantic_ai_lightspeed/capabilities/utils.py index 2461fc6fa..6f8775233 100644 --- a/src/pydantic_ai_lightspeed/capabilities/utils.py +++ b/src/pydantic_ai_lightspeed/capabilities/utils.py @@ -17,6 +17,7 @@ def extract_message_str_from_user_content( Returns: A single string with all text content joined by newlines. + """ str_arr: list[str] = [] for c in user_content: @@ -37,6 +38,7 @@ def message_to_str(message: Optional[str | Sequence[UserContent]]) -> str: Returns: A plain-text representation of the message, or an empty string for None. + """ match message: case str() as s: @@ -60,6 +62,7 @@ def extract_conversation_id(model: Model) -> Optional[str]: Returns: The conversation ID, or None if the model has no such setting. + """ extra_body = (model.settings or {}).get("extra_body") if not isinstance(extra_body, dict): diff --git a/src/pydantic_ai_lightspeed/ogx/_model.py b/src/pydantic_ai_lightspeed/ogx/_model.py index d5d21bc24..97fe98b2c 100644 --- a/src/pydantic_ai_lightspeed/ogx/_model.py +++ b/src/pydantic_ai_lightspeed/ogx/_model.py @@ -131,6 +131,7 @@ def __init__( object when OGX emits ``response.completed``. Used to capture the structured output items for compacted-mode persistence (LCORE-3883). + """ self._source = source self._on_completed = on_completed @@ -197,6 +198,7 @@ def _replay_buffered_deltas( Returns: List of buffered delta events to yield, unchanged. + """ buffered = self._buffered_deltas.pop(item_id, []) if buffered: @@ -223,6 +225,7 @@ def _replay_mcp_buffered_deltas( Returns: List containing one synthetic delta event, or empty if nothing buffered. + """ buffered = self._buffered_deltas.pop(item_id, []) if not buffered: @@ -263,6 +266,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: Args: *args: Positional arguments forwarded to ``OpenAIResponsesModel``. **kwargs: Keyword arguments forwarded to ``OpenAIResponsesModel``. + """ super().__init__(*args, **kwargs) self._last_output_items: list[Any] = [] @@ -283,6 +287,7 @@ def _capture_output_items(self, response: Any) -> None: Args: response: The OGX Responses API object for the completed call. + """ items = getattr(response, "output", None) if items: @@ -309,6 +314,7 @@ async def _responses_create( Returns: Response from the Responses API. + """ # Parent gates include on this profile flag; disable it for OGX. self.profile["openai_supports_encrypted_reasoning_content"] = False @@ -440,6 +446,7 @@ async def request_stream( # pylint: disable=unused-argument Yields: A StreamedResponse with the filtered event stream. + """ check_allow_model_requests() messages, model_settings = self._prepare_conversation_continuation( @@ -523,6 +530,7 @@ def from_ogx_client( Returns: Configured ``OgxResponsesModel`` instance. + """ provider = OgxProvider.from_ogx_client(client) diff --git a/src/pydantic_ai_lightspeed/ogx/_provider.py b/src/pydantic_ai_lightspeed/ogx/_provider.py index 419e3e979..d8a9a70ab 100644 --- a/src/pydantic_ai_lightspeed/ogx/_provider.py +++ b/src/pydantic_ai_lightspeed/ogx/_provider.py @@ -66,6 +66,7 @@ def from_ogx_client( Returns: Configured ``OgxProvider`` instance. + """ if isinstance(client, AsyncOGXAsLibraryClient): return OgxProvider(library_client=client) @@ -103,6 +104,7 @@ def __init__( Mutually exclusive with ``base_url``, ``api_key``, and ``http_client``. http_client: An existing ``httpx.AsyncClient`` to use for making HTTP requests. Must be ``None`` when ``library_client`` is provided. + """ if library_client is not None: if base_url is not None: @@ -148,5 +150,6 @@ def _set_http_client(self, http_client: httpx.AsyncClient) -> None: Args: http_client: The async HTTP client to use for subsequent requests. + """ self._client._client = http_client # pyright: ignore[reportPrivateUsage] # pylint: disable=protected-access diff --git a/src/pydantic_ai_lightspeed/ogx/_transport.py b/src/pydantic_ai_lightspeed/ogx/_transport.py index 77f529755..e9e2d7e2a 100644 --- a/src/pydantic_ai_lightspeed/ogx/_transport.py +++ b/src/pydantic_ai_lightspeed/ogx/_transport.py @@ -30,6 +30,7 @@ def decode_request_headers(request: httpx.Request) -> dict[str, str]: Returns: Request headers with byte keys and values decoded to UTF-8 strings. + """ return { k.decode("utf-8") if isinstance(k, bytes) else k: ( @@ -51,6 +52,7 @@ def inject_provider_data_into_headers( Returns: Headers with provider data injected when absent from the request. + """ if not provider_data: return dict(headers) @@ -73,6 +75,7 @@ def request_with_provider_data_headers( Returns: The original request, or a copy with provider data headers added. + """ headers = inject_provider_data_into_headers( decode_request_headers(request), @@ -102,6 +105,7 @@ def wrap_http_client_with_provider_data( Returns: The original client when ``provider_data`` is empty, otherwise a new client with a wrapping transport. + """ if not provider_data: return http_client @@ -131,6 +135,7 @@ def __init__( Args: transport: The underlying transport used for real HTTP requests. provider_data: Provider credentials/metadata to forward to OGX. + """ self._transport = transport self._provider_data = provider_data @@ -143,6 +148,7 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response: Returns: The response from the wrapped transport. + """ request = request_with_provider_data_headers(request, self._provider_data) return await self._transport.handle_async_request(request) @@ -160,6 +166,7 @@ def __init__(self, gen: AsyncGenerator[bytes, None]) -> None: Args: gen: An async generator producing byte chunks to stream. + """ self._gen = gen @@ -168,6 +175,7 @@ async def __aiter__(self) -> AsyncIterator[bytes]: Returns: An async iterator of bytes fulfilling the httpx.AsyncByteStream contract. + """ async for chunk in self._gen: yield chunk @@ -187,6 +195,7 @@ def __init__(self, client: AsyncOGXAsLibraryClient) -> None: Args: client: An initialized ``AsyncOGXAsLibraryClient`` whose route handlers will receive dispatched requests. + """ self._client = client @@ -201,6 +210,7 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response: Raises: RuntimeError: If the library client has not been initialized. + """ if self._client.route_impls is None: raise RuntimeError( @@ -244,6 +254,7 @@ async def _handle_non_streaming( Raises: RuntimeError: If route_impls is not initialized. + """ if self._client.route_impls is None: raise RuntimeError("route_impls is not initialized") @@ -292,6 +303,7 @@ async def _handle_streaming( Raises: RuntimeError: If route_impls is not initialized. + """ if self._client.route_impls is None: raise RuntimeError("route_impls is not initialized") diff --git a/src/quota/cluster_quota_limiter.py b/src/quota/cluster_quota_limiter.py index f378f2aef..0220509c9 100644 --- a/src/quota/cluster_quota_limiter.py +++ b/src/quota/cluster_quota_limiter.py @@ -28,6 +28,7 @@ def __init__( Notes: ----- Establishes the database connection and ensures required tables exist. + """ subject = "c" # cluster super().__init__(configuration, initial_quota, increase_by, subject) @@ -44,6 +45,7 @@ def __str__(self) -> str: A string containing the class name and the values of `initial_quota` and `increase_by` specified in the service configuration. + """ name = type(self).__name__ return f"{name}: initial quota: {self.initial_quota} increase by: {self.increase_by}" diff --git a/src/quota/connect_pg.py b/src/quota/connect_pg.py index e74700a5d..81c056bab 100644 --- a/src/quota/connect_pg.py +++ b/src/quota/connect_pg.py @@ -27,6 +27,7 @@ def connect_pg(config: PostgreSQLDatabaseConfiguration) -> Any: Raises: ------ psycopg2.Error: If establishing the database connection fails. + """ logger.info("Connecting to PostgreSQL storage") namespace = "public" diff --git a/src/quota/connect_sqlite.py b/src/quota/connect_sqlite.py index a745f57cb..1ba6025b1 100644 --- a/src/quota/connect_sqlite.py +++ b/src/quota/connect_sqlite.py @@ -25,6 +25,7 @@ def connect_sqlite(config: SQLiteDatabaseConfiguration) -> Any: Raises: ------ sqlite3.Error: If establishing the connection fails. + """ logger.info("Connecting to SQLite storage") # make sure the connection will have known state diff --git a/src/quota/quota_exceed_error.py b/src/quota/quota_exceed_error.py index 724afe9bb..1f7f2e411 100644 --- a/src/quota/quota_exceed_error.py +++ b/src/quota/quota_exceed_error.py @@ -26,6 +26,7 @@ def __init__( subject_id (str): Copied from the `subject_id` parameter. available (int): Copied from the `available` parameter. needed (int): Copied from the `needed` parameter. + """ message: str = "" diff --git a/src/quota/quota_limiter.py b/src/quota/quota_limiter.py index af593a36e..56c3b000a 100644 --- a/src/quota/quota_limiter.py +++ b/src/quota/quota_limiter.py @@ -61,6 +61,7 @@ def available_quota(self, subject_id: str) -> int: Returns: ------- available_quota (int): Number of quota units currently available for the subject. + """ @abstractmethod @@ -102,6 +103,7 @@ def consume_tokens( output_tokens (int): Number of output tokens to deduct from the subject's quota. subject_id (str): Identifier of the subject (user or service) whose quota will be reduced. If omitted, applies to the default subject. + """ @abstractmethod @@ -116,6 +118,7 @@ def __init__(self) -> None: postgres_connection_config (Optional[PostgreSQLDatabaseConfiguration]): PostgreSQL connection configuration or `None` when not configured. + """ self.sqlite_connection_config: Optional[SQLiteDatabaseConfiguration] = None self.postgres_connection_config: Optional[PostgreSQLDatabaseConfiguration] = ( @@ -174,6 +177,7 @@ def connected(self) -> bool: Returns: `true` if the connection is alive, `false` otherwise. + """ if self.connection is None: logger.warning("Not connected, need to reconnect later") diff --git a/src/quota/quota_limiter_factory.py b/src/quota/quota_limiter_factory.py index 6e86e8d31..29c890423 100644 --- a/src/quota/quota_limiter_factory.py +++ b/src/quota/quota_limiter_factory.py @@ -30,6 +30,7 @@ def quota_limiters(config: QuotaHandlersConfiguration) -> list[QuotaLimiter]: list[QuotaLimiter]: List of initialized quota limiter instances. Returns an empty list if storage configuration or limiter definitions are missing. + """ limiters: list[QuotaLimiter] = [] @@ -83,6 +84,7 @@ def create_limiter( Raises: ------ ValueError: If `limiter_type` is not a recognized limiter identifier. + """ match limiter_type: case constants.USER_QUOTA_LIMITER: diff --git a/src/quota/revokable_quota_limiter.py b/src/quota/revokable_quota_limiter.py index 44ae642de..0dec22661 100644 --- a/src/quota/revokable_quota_limiter.py +++ b/src/quota/revokable_quota_limiter.py @@ -47,6 +47,7 @@ def __init__( subject_type (str): Identifier for the kind of subject the limiter applies to (e.g., user, customer); when set to "c" the limiter treats subject IDs as empty strings. + """ self.subject_type = subject_type self.initial_quota = initial_quota @@ -69,6 +70,7 @@ def available_quota(self, subject_id: str = "") -> int: Returns: ------- int: The available quota for the subject. Returns 0 if no backend is configured. + """ if self.subject_type == "c": subject_id = "" @@ -96,6 +98,7 @@ def _read_available_quota(self, query_statement: str, subject_id: str) -> int: ------- int: The available quota for the subject; `initial_quota` if a new record was initialized. + """ # it is not possible to use context manager there, because SQLite does # not support it @@ -123,6 +126,7 @@ def revoke_quota(self, subject_id: str = "") -> None: subject_id (str): Identifier of the subject whose quota will be revoked. If the limiter's `subject_type` is `"c"`, this value is ignored and treated as an empty string. + """ if self.subject_type == "c": subject_id = "" @@ -146,6 +150,7 @@ def _revoke_quota(self, set_statement: str, subject_id: str) -> None: and `revoked_at` for a subject. subject_id (str): Identifier of the subject whose quota will be revoked. + """ # timestamp to be used revoked_at = datetime.now(tz=UTC) @@ -170,6 +175,7 @@ def increase_quota(self, subject_id: str = "") -> None: increased. When the limiter's `subject_type` is `"c"`, this value is normalized to the empty string and treated as a global/customer-level entry. + """ if self.subject_type == "c": subject_id = "" @@ -196,6 +202,7 @@ def _increase_quota(self, set_statement: str, subject_id: str) -> None: ---------- set_statement (str): SQL statement that increments the available quota for a subject. subject_id (str): Identifier of the subject whose quota will be increased. + """ # timestamp to be used updated_at = datetime.now(tz=UTC) @@ -222,6 +229,7 @@ def ensure_available_quota(self, subject_id: str = "") -> None: ------ QuotaExceedError: If the available quota for the subject is less than or equal to zero. + """ if self.subject_type == "c": subject_id = "" @@ -253,6 +261,7 @@ def consume_tokens( input_tokens (int): Number of input tokens to consume. output_tokens (int): Number of output tokens to consume. subject_id (str): Identifier of the subject whose quota will be consumed. + """ if self.subject_type == "c": subject_id = "" @@ -299,6 +308,7 @@ def _consume_tokens( The function updates the quota by -(input_tokens + output_tokens) and stamps the record with the current datetime, then commits the change. + """ # timestamp to be used updated_at = datetime.now(tz=UTC) @@ -343,6 +353,7 @@ def _init_quota(self, subject_id: str = "") -> None: ---------- subject_id (str): Identifier of the subject whose quota to initialize. Defaults to empty string. + """ # timestamp to be used revoked_at = datetime.now(tz=UTC) diff --git a/src/quota/token_usage_history.py b/src/quota/token_usage_history.py index 6ea5bcb40..d905de495 100644 --- a/src/quota/token_usage_history.py +++ b/src/quota/token_usage_history.py @@ -46,6 +46,7 @@ def __init__(self, configuration: QuotaHandlersConfiguration) -> None: ---------- configuration (QuotaHandlersConfiguration): Configuration containing `sqlite` and `postgres` connection settings. + """ # store the configuration, it will be used # by reconnection logic later, if needed @@ -73,6 +74,7 @@ def connect(self) -> None: Raises: ValueError: If neither PostgreSQL nor SQLite configuration is provided. + """ logger.info("Initializing connection to quota usage history database") if self.postgres_connection_config is not None: @@ -115,6 +117,7 @@ def consume_tokens( # pylint: disable=too-many-arguments,too-many-positional-ar Raises: ------ ValueError: If no database backend configuration (Postgres or SQLite) is available. + """ logger.info( "Token usage for user %s, provider %s and model %s changed by %d, %d tokens", @@ -162,6 +165,7 @@ def connected(self) -> bool: Returns: `true` if the database connection is present and responds to a simple query, `false` otherwise. + """ if self.connection is None: logger.warning("Not connected, need to reconnect later") diff --git a/src/quota/user_quota_limiter.py b/src/quota/user_quota_limiter.py index 67cea6bfc..5782bd6e3 100644 --- a/src/quota/user_quota_limiter.py +++ b/src/quota/user_quota_limiter.py @@ -30,6 +30,7 @@ def __init__( ----- Establishes the database connection and initializes required tables as part of construction. + """ subject = "u" # user super().__init__(configuration, initial_quota, increase_by, subject) @@ -46,6 +47,7 @@ def __str__(self) -> str: A string containing the class name and the values of `initial_quota` and `increase_by` specified in the service configuration. + """ name = type(self).__name__ return f"{name}: initial quota: {self.initial_quota} increase by: {self.increase_by}" diff --git a/src/runners/quota_scheduler.py b/src/runners/quota_scheduler.py index 3d260b07b..931d60dad 100644 --- a/src/runners/quota_scheduler.py +++ b/src/runners/quota_scheduler.py @@ -42,6 +42,7 @@ def quota_scheduler(config: QuotaHandlersConfiguration) -> bool: bool: `True` if the scheduler started (unreachable in normal execution because the function enters an infinite loop), `False` if validation failed or a database connection could not be established. + """ if config is None: logger.warning("Quota limiters are not configured, skipping") @@ -131,6 +132,7 @@ def connected(connection: Any) -> bool: Returns: ------- bool: True if connection is active, False otherwise. + """ if connection is None: logger.warning("Not connected, need to reconnect later") @@ -159,6 +161,7 @@ def get_increase_quota_statement(config: QuotaHandlersConfiguration) -> str: Returns: ------- str: SQL statement to perform a quota increase appropriate for the configured backend. + """ if config.sqlite is not None: return INCREASE_QUOTA_STATEMENT_SQLITE @@ -172,6 +175,7 @@ def get_reset_quota_statement(config: QuotaHandlersConfiguration) -> str: Returns: str: The SQLite reset SQL statement when `config.sqlite` is set, otherwise the PostgreSQL reset SQL statement. + """ if config.sqlite is not None: return RESET_QUOTA_STATEMENT_SQLITE @@ -202,6 +206,7 @@ def quota_revocation( Raises: ------ ValueError: If the limiter's `type` or `period` is not set. + """ logger.info( "Quota revocation mechanism for limiter '%s' of type '%s'", @@ -261,6 +266,7 @@ def increase_quota( (e.g., "u" for user, "c" for cluster). increase_by (int): Amount to add to the subject's quota. period (str): Quota period identifier used to scope the update. + """ logger.info( "Increasing quota for subject '%s' by %d when period %s is reached", @@ -303,6 +309,7 @@ def reset_quota( subject_id (str): Identifier for the quota subject (e.g., "u" for user, "c" for cluster). reset_to (int): Value to set the subject's quota to. period (str): Period identifier for which the quota is being set. + """ logger.info( "Resetting quota for subject '%s' to %d when period %s is reached", @@ -339,6 +346,7 @@ def get_subject_id(limiter_type: str) -> str: ------- str: `"u"` for a user limiter, `"c"` for a cluster limiter, or `"?"` if the type is not recognized. + """ match limiter_type: case constants.USER_QUOTA_LIMITER: @@ -362,6 +370,7 @@ def connect(config: QuotaHandlersConfiguration) -> Any: ------- A database connection suitable for quota operations, or `None` if neither Postgres nor SQLite is configured. + """ logger.info("Initializing connection to quota limiter database") if config.postgres is not None: @@ -380,6 +389,7 @@ def init_tables(connection: Any, create_quota_table: str) -> None: connection (Any): A DB-API compatible connection on which the quota table(s) will be created; changes are committed before returning. create_quota_table (str): Command used to create table with quota. + """ logger.info("Initializing tables for quota limiter") cursor = connection.cursor() @@ -396,6 +406,7 @@ def start_quota_scheduler(configuration: Configuration) -> None: ---------- configuration (Configuration): Global configuration whose `quota_handlers` attribute is passed to the scheduler thread. + """ logger.info("Starting quota scheduler") thread = Thread( diff --git a/src/runners/uvicorn.py b/src/runners/uvicorn.py index e170771d0..fa23a93b0 100644 --- a/src/runners/uvicorn.py +++ b/src/runners/uvicorn.py @@ -25,6 +25,7 @@ def start_uvicorn( and will be forwarded to uvicorn.run as provided. log_config (Optional[dict]): Logging configuration dictionary passed to uvicorn.run. When None, defaults to the output of setup_logging(). + """ log_level = resolve_log_level() logger.info("Starting Uvicorn with log level %s", logging.getLevelName(log_level)) diff --git a/src/telemetry/configuration_snapshot.py b/src/telemetry/configuration_snapshot.py index 66f0c73ce..1940e57bf 100644 --- a/src/telemetry/configuration_snapshot.py +++ b/src/telemetry/configuration_snapshot.py @@ -42,6 +42,7 @@ class MaskingType(Enum): user-chosen rag_ids (potential PII), so only the count is emitted; the fixed OKP sentinel is surfaced as a boolean so telemetry can tell whether the OKP knowledge source is in use. + """ PASSTHROUGH = "passthrough" @@ -56,6 +57,7 @@ class FieldSpec: Attributes: path: Dotted path to the field in the configuration object. masking: Type of masking to apply to the field value. + """ path: str @@ -69,6 +71,7 @@ class ListFieldSpec: Attributes: path: Dotted path to the list field in the configuration object. item_fields: Sub-field specifications to extract from each list item. + """ path: str @@ -458,6 +461,7 @@ def get_nested_value(obj: Any, path: str) -> Any: Returns: ------- The value at the specified path, or None if not found. + """ current = obj for part in path.split("."): @@ -480,6 +484,7 @@ def _serialize_passthrough(value: Any) -> Any: Returns: ------- A JSON-serializable representation of the value. + """ if value is None: return None @@ -514,6 +519,7 @@ def _summarize_rag_sources(value: Any) -> dict[str, Any]: Returns: ------- A dict {'count': int, 'okp_enabled': bool}. + """ if not isinstance(value, (list, tuple)): return {"count": 0, "okp_enabled": False} @@ -534,6 +540,7 @@ def mask_value(value: Any, masking: MaskingType) -> Any: Returns: ------- The masked or serialized value. + """ if masking == MaskingType.SENSITIVE: if value is None or value == "": @@ -552,6 +559,7 @@ def _set_nested_value(target: dict[str, Any], path: str, value: Any) -> None: target: The target dict to modify. path: Dotted path where the value should be set. value: The value to set. + """ parts = path.split(".") current = target @@ -573,6 +581,7 @@ def _extract_field(source: Any, spec: FieldSpec) -> Any: Returns: ------- The masked value of the field. + """ value = get_nested_value(source, spec.path) return mask_value(value, spec.masking) @@ -592,6 +601,7 @@ def _extract_list_field( ------- A list of dicts with masked sub-fields, or NOT_CONFIGURED if the list is None. + """ items = get_nested_value(source, spec.path) if items is None: @@ -631,6 +641,7 @@ def _extract_snapshot_fields( Returns: ------- A nested dict containing the extracted and masked fields. + """ snapshot: dict[str, Any] = {} for spec in field_registry: @@ -661,6 +672,7 @@ def _extract_store_info(ogx_config: dict[str, Any], store_name: str) -> dict[str Returns: ------- A dict with 'type' and 'db_path' keys, plus 'namespace' for metadata store. + """ store = get_nested_value(ogx_config, f"storage.stores.{store_name}") if store is None or not isinstance(store, dict): @@ -705,6 +717,7 @@ def build_lightspeed_stack_snapshot( Returns: ------- A nested dict containing the masked configuration snapshot. + """ return _extract_snapshot_fields(config, LIGHTSPEED_STACK_FIELDS) @@ -719,6 +732,7 @@ def _read_yaml_file(config_path: str) -> Any: Returns: ------- The parsed YAML content, or None on failure. + """ try: with open(config_path, encoding="utf-8") as f: @@ -746,6 +760,7 @@ async def build_ogx_snapshot( ------- A nested dict containing the masked OGX configuration snapshot, or a status dict if the config is not available. + """ if config_path is None: return {"status": NOT_AVAILABLE} @@ -783,6 +798,7 @@ async def build_configuration_snapshot( ------- A dict with 'lightspeed_stack' and 'ogx' keys containing the respective masked snapshots, ready for JSON serialization. + """ return { "lightspeed_stack": build_lightspeed_stack_snapshot(config), diff --git a/src/transcripts/transcripts.py b/src/transcripts/transcripts.py index b7cbe4b9a..6060ec65e 100644 --- a/src/transcripts/transcripts.py +++ b/src/transcripts/transcripts.py @@ -36,6 +36,7 @@ def _hash_user_id(user_id: str) -> str: Returns: ------- str: Hexadecimal SHA-256 digest of the UTF-8 encoded user_id. + """ return hashlib.sha256(user_id.encode("utf-8")).hexdigest() @@ -58,6 +59,7 @@ def construct_transcripts_path(hashed_user_id: str, conversation_id: str) -> Pat ------- Path: A Path pointing to the directory where transcripts for the specified user and conversation should be stored. + """ # these two normalizations are required by Snyk as it detects # this Path sanitization pattern @@ -81,6 +83,7 @@ def store_transcript( Raises: ------ HTTPException: If writing the transcript file to disk fails. + """ transcripts_path = construct_transcripts_path( transcript.metadata.user_id, transcript.metadata.conversation_id @@ -121,6 +124,7 @@ def create_transcript_metadata( # pylint: disable=too-many-arguments,too-many-p Returns: ------- TranscriptMetadata: A TranscriptMetadata BaseModel instance. + """ hashed_user_id = _hash_user_id(user_id) @@ -154,6 +158,7 @@ def create_transcript( Returns: ------- Transcript: A Transcript BaseModel instance ready to be stored. + """ return Transcript( metadata=metadata, diff --git a/src/utils/agents/error_handler.py b/src/utils/agents/error_handler.py index 9166d7f52..4098bb947 100644 --- a/src/utils/agents/error_handler.py +++ b/src/utils/agents/error_handler.py @@ -47,6 +47,7 @@ def map_agent_inference_error( Raises: RuntimeError: Re-raised when ``exc`` is a non-agent ``RuntimeError`` that is not a recognized context-length failure. + """ match exc: case AgentRunError() as agent_exc: @@ -91,6 +92,7 @@ def map_pydantic_agent_run_error( # pylint: disable=too-many-return-statements Returns: Structured error response for HTTP or SSE error events. + """ match exc: case ContentFilterError() as filter_exc: diff --git a/src/utils/agents/query.py b/src/utils/agents/query.py index 8b6db71c3..a7b2d578a 100644 --- a/src/utils/agents/query.py +++ b/src/utils/agents/query.py @@ -78,6 +78,7 @@ def get_agent_finish_reason(response: ModelResponse) -> AgentFinishReason: Returns: Resolved finish reason. + """ raw_finish_reason = (response.provider_details or {}).get("finish_reason") if raw_finish_reason == "cancelled": @@ -99,6 +100,7 @@ def get_finish_reason_error( Returns: Structured error response for HTTP or SSE error events. + """ match finish_reason: case AgentFinishReason.LENGTH: @@ -131,6 +133,7 @@ def extract_agent_token_usage( Returns: Aggregated token usage counter for the run. + """ provider_id, model_id = extract_provider_and_model_from_model_id(model) token_counter = TokenCounter( @@ -177,6 +180,7 @@ def build_turn_summary_from_agent_run( Raises: HTTPException: When the run failed. + """ finish_reason = get_agent_finish_reason(run_result.response) if finish_reason != AgentFinishReason.SUCCESS: @@ -253,6 +257,7 @@ async def retrieve_agent_response( Raises: HTTPException: On agent or provider failure. + """ with tracer.start_as_current_span("llm.inference") as span: # Extract provider and model from model_id diff --git a/src/utils/agents/streaming.py b/src/utils/agents/streaming.py index 49637c92d..c179733f7 100644 --- a/src/utils/agents/streaming.py +++ b/src/utils/agents/streaming.py @@ -121,6 +121,7 @@ async def retrieve_agent_response_generator( Returns: Tuple of SSE async iterator and mutable turn summary. + """ turn_summary = TurnSummary() try: @@ -169,6 +170,7 @@ async def _persist_compacted_turn( ``None`` the request was not compacted and nothing is written. persist_guard: Single-element flag shared with the interrupt path, so a turn is persisted at most once. + """ if original_input is None or persist_guard[0]: return @@ -224,6 +226,7 @@ async def generate_agent_response( # pylint: disable=too-many-statements Yields: SSE-formatted strings from the wrapped generator. + """ media_type = context.query_request.media_type or MEDIA_TYPE_JSON persist_guard = register_interrupt_callback( @@ -395,6 +398,7 @@ async def agent_response_generator( Yields: Serialized SSE event strings. + """ with tracer.start_as_current_span("llm.inference") as span: provider_id, model_id = extract_provider_and_model_from_model_id( @@ -510,6 +514,7 @@ def _process_token( Returns: Token stream payload containing the emitted token chunk. + """ state.text_parts.append(text) state.turn_summary.partial_tokens.append(text) @@ -535,6 +540,7 @@ def dispatch_stream_event( # pylint: disable=useless-return Returns: None when the event does not map to an SSE payload. + """ logger.debug("Ignoring event kind=%s", event.event_kind) @@ -554,6 +560,7 @@ def _( Returns: Completion stream payload. + """ state.run_result = event.result state.turn_summary.id = state.run_result.response.provider_response_id or "" @@ -587,6 +594,7 @@ def _( Returns: Optional stream payload emitted at part start. + """ part = event.part if isinstance(part, TextPart): @@ -615,6 +623,7 @@ def _( Returns: Optional stream payload emitted for the delta. + """ delta = event.delta if isinstance(delta, TextPartDelta): @@ -637,6 +646,7 @@ def _( Returns: Optional stream payload emitted at part end. + """ part = event.part if isinstance(part, TextPart): @@ -668,6 +678,7 @@ def _( Returns: Tool call stream payload or None. + """ if summary := process_function_tool_call(state, event.part): return ToolCallStreamPayload(data=summary) @@ -687,6 +698,7 @@ def _( Returns: Tool result stream payload or None. + """ part = event.part if not isinstance(part, ToolReturnPart): diff --git a/src/utils/agents/tool_processor.py b/src/utils/agents/tool_processor.py index 1be1854f5..e79363266 100644 --- a/src/utils/agents/tool_processor.py +++ b/src/utils/agents/tool_processor.py @@ -51,6 +51,7 @@ def summarize_function_tool_call(part: ToolCallPart) -> ToolCallSummary: Returns: Tool call summary in LCS turn-summary format. + """ return ToolCallSummary( id=part.tool_call_id, @@ -70,6 +71,7 @@ def summarize_native_tool_call( Returns: Tool call summary in LCS turn-summary format. + """ call_id = part.tool_call_id args = part.args_as_dict() @@ -127,6 +129,7 @@ def process_function_tool_call( Returns: Tool call summary when recorded, otherwise None if already emitted. + """ if part.tool_call_id in state.emitted_tool_call_ids: return None @@ -149,6 +152,7 @@ def process_native_tool_call( Returns: Tool call summary when recorded, otherwise None if already emitted. + """ if part.tool_call_id in state.emitted_tool_call_ids: return None @@ -175,6 +179,7 @@ def process_native_tool_result( or the call was denied (e.g. by a TOOL-point guardrail violation): the call itself still surfaces via ``process_native_tool_call``, but a denied call's result is never recorded. + """ if part.tool_call_id in state.emitted_tool_result_ids: return None @@ -223,6 +228,7 @@ def process_function_tool_result( or the call was denied (e.g. by a TOOL-point guardrail violation): the call itself still surfaces via ``process_function_tool_call``, but a denied call's result is never recorded. + """ if part.tool_call_id in state.emitted_tool_result_ids: return None @@ -247,6 +253,7 @@ def summarize_function_tool_result( Returns: Tool result summary in LCS turn-summary format. + """ return ToolResultSummary( id=part.tool_call_id, @@ -273,6 +280,7 @@ def referenced_documents_from_file_search_results( Returns: Newly discovered referenced documents from these result rows. + """ documents: list[ReferencedDocument] = [] for result in results: @@ -304,6 +312,7 @@ def build_referenced_document( Returns: Referenced document when metadata is present, otherwise None. + """ attributes = result.attributes or {} resolved_source = resolve_source_for_result( @@ -343,6 +352,7 @@ def _file_search_attribute_str( Returns: Non-empty string value for the key, or None. + """ return str(value) if (value := attributes.get(key)) else None @@ -357,6 +367,7 @@ def _file_search_attribute_url( Returns: First matching URL value as a string, or None. + """ for key in _FILE_SEARCH_URL_KEYS: if url := _file_search_attribute_str(attributes, key): @@ -378,6 +389,7 @@ def rag_chunks_from_file_search_results( Returns: RAG chunks extracted from these result rows. + """ return [ RAGChunk( @@ -405,6 +417,7 @@ def summarize_web_search_result( Returns: Tool result summary in LCS turn-summary format. + """ content = cast("dict[str, Any]", part.content) status = str(content.pop("status")) @@ -429,6 +442,7 @@ def summarize_mcp_list_tools_result( Returns: Tool result summary in LCS turn-summary format. + """ content = cast("dict[str, Any]", part.content) call_id = part.tool_call_id @@ -468,6 +482,7 @@ def summarize_mcp_call_result( Returns: Tool result summary in LCS turn-summary format. + """ content = cast("dict[str, Any]", part.content) call_id = part.tool_call_id @@ -505,6 +520,7 @@ def summarize_mcp_tool_result( Returns: Tool result summary in LCS turn-summary format. + """ content = cast("dict[str, Any]", part.content) if "tools" in content: @@ -530,6 +546,7 @@ def summarize_file_search_result( Returns: Tool result summary, RAG chunks, and referenced documents for this return. + """ content = cast("dict[str, Any]", part.content) tool_result = ToolResultSummary( diff --git a/src/utils/builtin_tools.py b/src/utils/builtin_tools.py index fcac131a2..0c1f1f9bc 100644 --- a/src/utils/builtin_tools.py +++ b/src/utils/builtin_tools.py @@ -63,6 +63,7 @@ def _is_file_search_provider(provider: ProviderInfo) -> bool: Returns: True when the provider implements file-search tool runtime. + """ return ( provider.api == TOOL_RUNTIME_API @@ -85,6 +86,7 @@ async def get_file_search_tools( Returns: Catalog tools for the configured file-search runtime, or an empty list when file search is not configured. + """ try: providers = await client.providers.list() diff --git a/src/utils/checks.py b/src/utils/checks.py index 220258586..7d33a2d5b 100644 --- a/src/utils/checks.py +++ b/src/utils/checks.py @@ -35,6 +35,7 @@ def get_attribute_from_file(data: dict[str, str], file_name_key: str) -> Optiona ------- Optional[str]: File contents with trailing whitespace stripped, or None if the key is not present or is None. + """ file_path = data.get(file_name_key) if file_path is not None: @@ -60,6 +61,7 @@ def file_check(path: FilePath, desc: str) -> None: ------ InvalidConfigurationError: If `path` does not point to a file or is not readable. + """ if not os.path.isfile(path): raise InvalidConfigurationError(f"{desc} '{path}' is not a file") @@ -87,6 +89,7 @@ def directory_check( ------ InvalidConfigurationError: If `path` does not point to a directory or is not writable when required. + """ if not os.path.exists(path): if must_exists: @@ -113,6 +116,7 @@ def import_python_module(profile_name: str, profile_path: str) -> Optional[Modul Optional[ModuleType]: The loaded module on success; `None` if `profile_path` does not end with `.py`, if a module spec or loader cannot be created, or if importing/executing the module fails. + """ if not profile_path.endswith(".py"): return None @@ -146,6 +150,7 @@ def is_valid_profile(profile_module: ModuleType) -> bool: Returns: True if the module provides a dict `PROFILE_CONFIG` containing a `system_prompts` dict, False otherwise. + """ if not hasattr(profile_module, "PROFILE_CONFIG"): return False @@ -178,6 +183,7 @@ def is_valid_sqlite_period(period: str) -> bool: ------- bool: True if datetime() applied to a fixed timestamp with the period as the modifier returns a later time, False otherwise. + """ # julianday() makes it a comparison of times: as text, the result of the # modifier 'subsec' would be greater than the timestamp it does not move diff --git a/src/utils/common.py b/src/utils/common.py index e558e0e6f..f502b1d72 100644 --- a/src/utils/common.py +++ b/src/utils/common.py @@ -19,6 +19,7 @@ def run_once_async(func: Callable[..., Any]) -> Callable[..., Any]: Returns: Any: The result produced by the wrapped coroutine, or the exception it raised propagated to callers. + """ task = None @@ -35,6 +36,7 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any: Returns: The awaited result of the wrapped coroutine. + """ nonlocal task if task is None: diff --git a/src/utils/compaction.py b/src/utils/compaction.py index 47c2c9530..f0c98db69 100644 --- a/src/utils/compaction.py +++ b/src/utils/compaction.py @@ -78,6 +78,7 @@ def format_conversation_for_summary(items: list[Any]) -> str: Returns: Multi-line string. One line per message in the form ``": "``. Empty when no message items contain text. + """ lines: list[str] = [] for item in items: @@ -136,6 +137,7 @@ def partition_conversation( ``(old_items, recent_items)``. ``old_items`` may be empty (no compaction needed); ``recent_items`` may be empty (everything had to be summarized). + """ msg_indices = _message_indices(items) if not msg_indices: @@ -183,6 +185,7 @@ def _extract_response_text(response: Any) -> str: Returns: Concatenated response text, or the empty string when nothing could be extracted. + """ parts: list[str] = [] output = getattr(response, "output", None) or [] @@ -251,6 +254,7 @@ async def summarize_chunk( the resulting summary would be empty. A zero-token summary cannot be persisted (PositiveInt) and is also useless as context, so propagating an error is the honest behavior. + """ transcript = format_conversation_for_summary(old_items) logger.info( @@ -357,6 +361,7 @@ async def recursively_resummarize( ValueError: When *summaries* has fewer than two entries (no fold is needed) or when the LLM call yields no extractable text. + """ if len(summaries) < 2: raise ValueError( diff --git a/src/utils/connection_decorator.py b/src/utils/connection_decorator.py index a572c8d68..d288d30ae 100644 --- a/src/utils/connection_decorator.py +++ b/src/utils/connection_decorator.py @@ -53,6 +53,7 @@ def connection( def list_history(self) -> list[str]: pass ``` + """ def wrapper(self: S, *args: P.args, **kwargs: P.kwargs) -> R: @@ -69,6 +70,7 @@ def wrapper(self: S, *args: P.args, **kwargs: P.kwargs) -> R: Returns: ------- Any: The value returned by the wrapped callable. + """ if not self.connected(): self.connect() diff --git a/src/utils/conversation_compaction.py b/src/utils/conversation_compaction.py index 1b9f7c480..8f85f8f69 100644 --- a/src/utils/conversation_compaction.py +++ b/src/utils/conversation_compaction.py @@ -165,6 +165,7 @@ class CompactionStartedEvent: Attributes: conversation_id: The conversation being compacted (OGX format). + """ conversation_id: str @@ -190,6 +191,7 @@ class CompactionResult: caller must append this plus the LLM output to the conversation items itself, since the ``conversation`` parameter is no longer passed to OGX. + """ params: ResponsesApiParams @@ -335,6 +337,7 @@ def exclude_marker_items(items: Sequence[Any]) -> list[Any]: Returns: The items that are not markers, in their original order. + """ return [ item @@ -360,6 +363,7 @@ def agent_prompt_text(params: ResponsesApiParams) -> str: ``params.input`` unchanged when it is a string; otherwise the text of the last message item in the explicit list, or ``""`` when there is none. + """ if isinstance(params.input, str): return params.input @@ -403,6 +407,7 @@ def reject_image_attachments_in_compacted_mode( Raises: HTTPException: 422 when the turn is compacted and carries images. + """ if not image_attachments or not params.omit_conversation: return @@ -729,6 +734,7 @@ async def apply_compaction( # pylint: disable=too-many-arguments,too-many-posit Yields: Zero or more CompactionStartedEvent, then exactly one CompactionResult. + """ if not compaction_config.enabled: # ``enabled: false`` is a full off-switch: the request passes through @@ -876,6 +882,7 @@ async def needs_compaction_path( Returns: True if the compaction-aware streaming path should be used. + """ if not compaction_config.enabled: return False diff --git a/src/utils/conversations.py b/src/utils/conversations.py index dd25c24ce..471a2e843 100644 --- a/src/utils/conversations.py +++ b/src/utils/conversations.py @@ -79,6 +79,7 @@ def to_conversation_item(data: dict[str, Any]) -> Optional[ConversationItem]: Returns: ConversationItem if parsing succeeds, None otherwise. + """ try: return ConversationItem.from_dict(data) @@ -94,6 +95,7 @@ def build_add_items_request(items: Sequence[dict[str, Any]]) -> AddItemsRequest: Returns: Request body for items.create. + """ return AddItemsRequest( items=[ @@ -112,6 +114,7 @@ def _extract_text_from_content(content: Any) -> str: Returns: Extracted text content as a string + """ if isinstance(content, str): return content @@ -146,6 +149,7 @@ def _function_call_output_to_str(output: FunctionCallOutputContent) -> str: Returns: Plain string content for ``ToolResultSummary``. + """ if isinstance(output, str): return output @@ -168,6 +172,7 @@ def _parse_message_item(item: ConversationMessage) -> Message: Returns: Message object with extracted content and type (user or assistant) + """ return Message( content=_extract_text_from_content(item.content), @@ -187,6 +192,7 @@ def _build_tool_call_summary_from_item( # pylint: disable=too-many-return-state Returns: A tuple of (ToolCallSummary, ToolResultSummary) one of them possibly None if the item type doesn't provide both call and result information. + """ item_type = getattr(item, "type", None) @@ -352,6 +358,7 @@ def _create_dummy_turn_metadata(started_at: datetime) -> UserTurn: Returns: UserTurn instance with default values (N/A for provider/model, provided timestamp) for legacy conversations that don't have stored turn metadata. + """ # Create a UserTurn instance with default values for legacy conversations # Note: conversation_id and turn_number are not used, so we use placeholder values @@ -381,6 +388,7 @@ def _create_turn_from_db_metadata( Returns: ConversationTurn object with all metadata populated + """ started_at = turn_metadata.started_at.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%SZ") completed_at = turn_metadata.completed_at.astimezone(UTC).strftime( @@ -410,6 +418,7 @@ def _group_items_into_turns( Returns: List of turns, where each turn is a list of items belonging to that turn + """ turns: list[list[ConversationItem]] = [] current_turn_items: list[ConversationItem] = [] @@ -452,6 +461,7 @@ def _process_turn_items( Returns: Tuple of (messages, tool_calls, tool_results) + """ messages: list[Message] = [] tool_calls: list[ToolCallSummary] = [] @@ -491,6 +501,7 @@ def build_conversation_turns_from_items( Returns: List of ConversationTurn objects, oldest first + """ # Group items into turns first turn_items_list = _group_items_into_turns(items) @@ -539,6 +550,7 @@ async def append_turn_items_to_conversation( conversation_id: The OGX conversation ID. user_input: User input text or list of ResponseItem. llm_output: Output from the LLM: a list of OpenAIResponseOutput. + """ if isinstance(user_input, str): items: list[dict[str, Any]] = [ @@ -601,6 +613,7 @@ async def replace_last_assistant_message( client: The OGX client. conversation_id: The OGX conversation ID. replacement_message: The violation message to persist instead. + """ try: recent_items = await client.items.list( @@ -653,6 +666,7 @@ async def get_all_conversation_items( Returns: List of all items in the conversation, oldest first. + """ items: list[ConversationItem] = [] after: Optional[str] = None @@ -697,6 +711,7 @@ async def append_turn_to_conversation( conversation_id: The OGX conversation ID. user_message: The user's input message. assistant_message: The shield violation response message. + """ try: await client.items.create( diff --git a/src/utils/degraded_mode.py b/src/utils/degraded_mode.py index 99ea1aff4..574eadc27 100644 --- a/src/utils/degraded_mode.py +++ b/src/utils/degraded_mode.py @@ -28,6 +28,7 @@ def set_degraded(self, reason: str) -> None: Parameters: reason: Description of why degraded mode was entered. + """ self._is_degraded = True self._degraded_reason = reason @@ -48,6 +49,7 @@ def is_degraded(self) -> bool: Returns: True if service is in degraded mode, False otherwise. + """ return self._is_degraded @@ -56,5 +58,6 @@ def get_degraded_reason(self) -> Optional[str]: Returns: Description of why degraded mode was entered, or None if healthy. + """ return self._degraded_reason diff --git a/src/utils/dumpers/config_dumper.py b/src/utils/dumpers/config_dumper.py index 60e2ebc4e..501e7513a 100644 --- a/src/utils/dumpers/config_dumper.py +++ b/src/utils/dumpers/config_dumper.py @@ -18,6 +18,7 @@ def dump_schema(filename: str) -> None: Raises: ------ IOError: If the file cannot be written. + """ models = [Configuration] dump_openapi_schema(models, filename) diff --git a/src/utils/dumpers/models_dumper.py b/src/utils/dumpers/models_dumper.py index 733f29424..473ebe4cb 100644 --- a/src/utils/dumpers/models_dumper.py +++ b/src/utils/dumpers/models_dumper.py @@ -173,6 +173,7 @@ def dump_models(filename: str) -> None: Raises: ------ IOError: If the file cannot be written. + """ # construct a list with all models models = ( @@ -212,6 +213,7 @@ def get_models_for_group(model_group: str) -> list[type[BaseModel]]: Raises: ------ Exception: If model_group is not a recognized group name. + """ # pylint: disable=too-many-return-statements match model_group: @@ -247,6 +249,7 @@ def dump_models_group(model_group: str, filename: Optional[str] = None) -> None: Raises: ------ IOError: If the file cannot be written. + """ models = get_models_for_group(model_group) diff --git a/src/utils/dumpers/openapi_schema_dumper.py b/src/utils/dumpers/openapi_schema_dumper.py index 7773ad8c5..ea625e865 100644 --- a/src/utils/dumpers/openapi_schema_dumper.py +++ b/src/utils/dumpers/openapi_schema_dumper.py @@ -18,6 +18,7 @@ def dump_openapi_schema(models: list, filename: str) -> None: Raises: ------ IOError: If the file cannot be written. + """ with open(filename, "w", encoding="utf-8") as fout: _, schemas = models_json_schema( diff --git a/src/utils/endpoints.py b/src/utils/endpoints.py index 2fd454d34..1488dc258 100644 --- a/src/utils/endpoints.py +++ b/src/utils/endpoints.py @@ -35,6 +35,7 @@ def delete_conversation(conversation_id: str) -> bool: Returns: bool: True if the conversation was deleted, False if it was not found. + """ with get_session() as session: db_conversation = ( @@ -60,6 +61,7 @@ def retrieve_conversation(conversation_id: str) -> Optional[UserConversation]: Returns: Optional[UserConversation]: The conversation object if found, otherwise None. + """ with get_session() as session: return session.query(UserConversation).filter_by(id=conversation_id).first() @@ -76,6 +78,7 @@ def retrieve_conversation_turns(conversation_id: str) -> list[UserTurn]: Raises: HTTPException: 500 if a database error occurs. + """ try: with get_session() as session: @@ -106,6 +109,7 @@ def can_access_conversation( Returns: bool: True if the user is allowed to access the conversation, False otherwise. + """ if others_allowed: return True @@ -148,6 +152,7 @@ def validate_and_retrieve_conversation( - 403 Forbidden: If user doesn't have access to the conversation. - 404 Not Found: If conversation doesn't exist in database. - 500 Internal Server Error: If database error occurs. + """ if not can_access_conversation( normalized_conv_id, @@ -217,6 +222,7 @@ async def resolve_response_context( ------ HTTPException: 404 if previous_response_id is set but the turn does not exist; other HTTP exceptions from validate_and_retrieve_conversation. + """ client = AsyncOgxClientHolder().get_client() # Context for the LLM passed by conversation @@ -291,6 +297,7 @@ def retrieve_turn_by_response_id(response_id: str) -> UserTurn: Raises: HTTPException: 404 if no turn has this response_id; 500 on database error. + """ try: with get_session() as session: @@ -318,6 +325,7 @@ def check_turn_existence(response_id: str) -> bool: Returns: bool: True if the turn exists, False otherwise. + """ try: with get_session() as session: @@ -342,6 +350,7 @@ def check_configuration_loaded(config: AppConfig) -> None: Raises: HTTPException: If configuration is missing. + """ try: _ = config.configuration @@ -371,6 +380,7 @@ def _process_http_source( - `doc_title`: the last path segment of the URL or `src` if no path segment is present. Returns `None` if `src` was already present in `doc_urls`. + """ if src not in doc_urls: doc_urls.add(src) @@ -416,6 +426,7 @@ def _process_document_id( `validated_url` is a validated `AnyUrl` or `None` and `doc_title` is the chosen title string; returns `None` if the `src` or its URL was already processed. + """ if src in doc_ids: return None @@ -496,6 +507,7 @@ def _process_rag_chunks_for_documents( list[tuple[Optional[AnyUrl], str]]: Ordered list of tuples where the first element is a validated URL object or `None` (if no URL is available) and the second element is the document title. + """ doc_urls: set[str] = set() doc_ids: set[str] = set() @@ -553,6 +565,7 @@ def create_referenced_documents( Returns: ------- List of ReferencedDocument objects or dictionaries with doc_url and doc_title + """ document_entries = _process_rag_chunks_for_documents(rag_chunks, metadata_map) @@ -590,6 +603,7 @@ def create_referenced_documents_with_metadata( list[ReferencedDocument]: ReferencedDocument objects with `doc_url` and `doc_title` populated; `doc_url` may be `None` if no valid URL could be determined. + """ document_entries = _process_rag_chunks_for_documents( summary.rag_chunks, metadata_map @@ -618,6 +632,7 @@ def create_referenced_documents_from_chunks( list[ReferencedDocument]: ReferencedDocument instances created from the chunks; each contains `doc_url` (validated URL or `None`) and `doc_title`. + """ document_entries = _process_rag_chunks_for_documents(rag_chunks) return [ diff --git a/src/utils/input_sanitization.py b/src/utils/input_sanitization.py index 68abff81d..2d40dd45a 100644 --- a/src/utils/input_sanitization.py +++ b/src/utils/input_sanitization.py @@ -72,6 +72,7 @@ def normalize_unicode(text: str) -> str: Returns: NFC-normalized text. + """ return unicodedata.normalize("NFC", text) @@ -84,6 +85,7 @@ def _check_suspicious_unicode(text: str) -> Optional[str]: Returns: Description of the detected block, or None if clean. + """ for char in text: codepoint = ord(char) @@ -105,6 +107,7 @@ def _check_binary_encoding(text: str) -> Optional[str]: Returns: Description if binary encoding is detected, or None if clean. + """ if _BINARY_PATTERN.search(text): return "Input appears to contain binary-encoded content." @@ -119,6 +122,7 @@ def _check_hex_encoding(text: str) -> Optional[str]: Returns: Description if hex encoding is detected, or None if clean. + """ if _HEX_ESCAPE_PATTERN.search(text): return "Input appears to contain hex-encoded escape sequences." @@ -135,6 +139,7 @@ def _check_xml_injection(text: str) -> Optional[str]: Returns: Description if suspicious XML tags are detected, or None if clean. + """ if _XML_INJECTION_PATTERN.search(text): return "Input contains suspicious XML/markup injection tags." @@ -151,6 +156,7 @@ def detect_obfuscation(text: str) -> Optional[str]: Returns: Description of detected obfuscation, or None if the input is clean. + """ checks = [ _check_suspicious_unicode, @@ -180,6 +186,7 @@ def sanitize_input(text: str) -> tuple[str, Optional[str]]: normalized_text should be used for further processing. If rejection_reason is not None, the input should be rejected with the given reason. + """ normalized = normalize_unicode(text) rejection_reason = detect_obfuscation(normalized) diff --git a/src/utils/json_schema_updater.py b/src/utils/json_schema_updater.py index edbd613f8..4aaa56337 100644 --- a/src/utils/json_schema_updater.py +++ b/src/utils/json_schema_updater.py @@ -24,6 +24,7 @@ def recursive_update( ------- dict[str, Any]: A new schema dictionary with all OpenAPI-compatible transformations applied. + """ new: dict[str, Any] = {} for key, value in original.items(): diff --git a/src/utils/markdown_repair.py b/src/utils/markdown_repair.py index e0b7abfb8..8a75a1e02 100644 --- a/src/utils/markdown_repair.py +++ b/src/utils/markdown_repair.py @@ -54,6 +54,7 @@ def _process_html_tags(line: str, html_stack: list[str]) -> Optional[str]: Returns: The name of a raw tag that was opened and not closed on this line, or ``None`` if no raw-content zone was entered. + """ in_raw: Optional[str] = None @@ -97,6 +98,7 @@ def _find_raw_close(line: str, raw_tag: str) -> Optional[int]: Returns: The character position immediately after the closing tag, or ``None`` if no close tag was found. + """ for tag_match in _TAG_RE.finditer(line): if tag_match.group(1) == "/" and tag_match.group(2).lower() == raw_tag: @@ -117,6 +119,7 @@ def _raw_tag_open_at(line: str, position: int) -> bool: Returns: True if a raw-tag zone is open at *position*. + """ open_raw: Optional[str] = None for tag_match in _TAG_RE.finditer(line): @@ -148,6 +151,7 @@ def _strip_comments_with_zone_priority(line: str, in_comment: bool) -> tuple[str Returns: A tuple of (processed_line, updated_in_comment). + """ result: list[str] = [] remaining = line @@ -199,6 +203,7 @@ def _process_fence_match( Returns: Updated ``(in_code_fence, fence_char, fence_len)`` tuple. + """ group_3 = fence_match.group(3) matched_group = group_3 or fence_match.group(4) @@ -228,6 +233,7 @@ def close_open_markdown(text: str) -> str: Returns: A suffix string to append that closes open constructs. + """ if not text or not text.strip(): return "" diff --git a/src/utils/mcp/mcp_auth_headers.py b/src/utils/mcp/mcp_auth_headers.py index 1c08d34a6..9264f06c7 100644 --- a/src/utils/mcp/mcp_auth_headers.py +++ b/src/utils/mcp/mcp_auth_headers.py @@ -40,6 +40,7 @@ def resolve_authorization_headers( >>> # With client special case (kept as-is) >>> resolve_authorization_headers({"Authorization": "client"}) {"Authorization": "client"} + """ resolved: dict[str, str] = {} diff --git a/src/utils/mcp/mcp_headers.py b/src/utils/mcp/mcp_headers.py index f3473884f..6d368fe48 100644 --- a/src/utils/mcp/mcp_headers.py +++ b/src/utils/mcp/mcp_headers.py @@ -29,6 +29,7 @@ async def mcp_headers_dependency(request: Request) -> McpHeaders: Returns: ------- The mcp headers dictionary, or empty dictionary if not found or on json decoding error + """ return extract_mcp_headers(request) @@ -46,6 +47,7 @@ def extract_mcp_headers(request: Request) -> McpHeaders: Returns: ------- The mcp headers dictionary, or empty dictionary if not found or on json decoding error + """ mcp_headers_string = request.headers.get("MCP-HEADERS", "") mcp_headers = {} @@ -82,6 +84,7 @@ def handle_mcp_headers_with_toolgroups( Returns: Dictionary with URLs as keys and their corresponding headers as values + """ converted_mcp_headers = {} @@ -119,6 +122,7 @@ def extract_propagated_headers( Returns: Dictionary of header names to values extracted from the request. Only headers present in both the allowlist and the request are included. + """ lower_request_headers = {k.lower(): v for k, v in request_headers.items()} propagated: dict[str, str] = {} @@ -151,6 +155,7 @@ def strip_bearer_prefix(value: str) -> str: separating whitespace removed. Values without the scheme, including those with a differently-cased scheme (e.g. ``bearer``), are returned unchanged. + """ return value.removeprefix("Bearer ") @@ -169,6 +174,7 @@ def ensure_bearer_prefix(value: str) -> str: Returns: The value guaranteed to start with a single ``Bearer `` prefix. + """ return f"Bearer {strip_bearer_prefix(value)}" @@ -191,6 +197,7 @@ def find_unresolved_auth_headers( List of header names from ``configured`` that could not be resolved, i.e. are not present as a key in ``resolved``. An empty list means all headers were resolved successfully. + """ resolved_lower = {k.lower() for k in resolved} return [h for h in configured if h.lower() not in resolved_lower] @@ -222,6 +229,7 @@ def build_server_headers( Returns: Merged headers dictionary for the server. May be empty if no headers apply. + """ server_headers: dict[str, str] = dict(client_headers) existing_lower = {k.lower() for k in server_headers} @@ -283,6 +291,7 @@ def build_mcp_headers( Returns: McpHeaders keyed by MCP server name with the complete merged set of headers. Servers that end up with no headers are omitted from the result. + """ if not config.mcp_servers: return {} diff --git a/src/utils/mcp/mcp_oauth_probe.py b/src/utils/mcp/mcp_oauth_probe.py index 6fad35153..acce9ca67 100644 --- a/src/utils/mcp/mcp_oauth_probe.py +++ b/src/utils/mcp/mcp_oauth_probe.py @@ -48,6 +48,7 @@ async def check_mcp_auth( Raises: ------ HTTPException: 401 when an MCP server requires authentication (from probe_mcp). + """ complete_headers = build_mcp_headers( configuration, mcp_headers or {}, request_headers, token @@ -97,6 +98,7 @@ async def probe_mcp( HTTPException: 401 with WWW-Authenticate when the server returns 401 and includes that header; 401 without the header when the server returns 401 without it or when the probe fails (timeout/connection). + """ cause = f"MCP server at {url} requires OAuth" error_response = UnauthorizedResponse(cause=cause) diff --git a/src/utils/mcp/mcp_tools.py b/src/utils/mcp/mcp_tools.py index 997eb9c32..978573e85 100644 --- a/src/utils/mcp/mcp_tools.py +++ b/src/utils/mcp/mcp_tools.py @@ -128,6 +128,7 @@ async def list_mcp_tools( Returns: Tool definitions discovered from the MCP server, or an empty list when the server is unavailable or returns an error. + """ request_headers = _prepare_mcp_request_headers(headers) for index, (transport_name, list_via) in enumerate(_MCP_TRANSPORTS): diff --git a/src/utils/model_list.py b/src/utils/model_list.py index 745925bcf..253b61f59 100644 --- a/src/utils/model_list.py +++ b/src/utils/model_list.py @@ -28,6 +28,7 @@ def parse_openai_style_model(model: OpenAIModel) -> CatalogModel: Returns: CatalogModel: Normalized catalog entry. + """ custom_metadata = dict(model.custom_metadata or {}) model_type = custom_metadata.pop("model_type", None) or "unknown" @@ -53,6 +54,7 @@ def parse_anthropic_model(model: AnthropicModelInfo) -> CatalogModel: Returns: CatalogModel: Normalized catalog entry. Treated as an LLM. + """ metadata: dict[str, Any] = { "display_name": model.display_name, @@ -82,6 +84,7 @@ def parse_google_model(model: GoogleModelInfo) -> CatalogModel: Returns: CatalogModel: Normalized catalog entry. Treated as an LLM. + """ metadata: dict[str, Any] = { "display_name": model.display_name, @@ -110,6 +113,7 @@ def parse_model_list_response( Returns: list[CatalogModel]: Parsed models in the unified catalog shape. + """ match response.actual_instance: case OpenAIListModelsResponse(data=data): diff --git a/src/utils/ogx_serialization.py b/src/utils/ogx_serialization.py index 4912b150f..38576c541 100644 --- a/src/utils/ogx_serialization.py +++ b/src/utils/ogx_serialization.py @@ -15,5 +15,6 @@ def dump_ogx_model(obj: Any) -> Any: Returns: A JSON-serializable dict, list, or primitive produced from obj. + """ return _OGX_SERIALIZER.sanitize_for_serialization(obj) # type: ignore[no-untyped-call] diff --git a/src/utils/ogx_version.py b/src/utils/ogx_version.py index eb2a9ad86..0eef2f8f7 100644 --- a/src/utils/ogx_version.py +++ b/src/utils/ogx_version.py @@ -45,6 +45,7 @@ async def check_ogx_version( ApiException: If OGX is unreachable after all retries. InvalidOgxVersionException: If the detected version is outside the supported range or cannot be parsed. + """ if max_retries < 1: raise ValueError("max_retries must be >= 1") @@ -91,6 +92,7 @@ def compare_versions(version_info: str, minimal: str, maximal: str) -> None: ------ InvalidOgxVersionException: If `version_info` is outside the inclusive range defined by `minimal` and `maximal`. + """ version_pattern = r"\d+\.\d+\.\d+" match = re.search(version_pattern, version_info) diff --git a/src/utils/otel_tracing.py b/src/utils/otel_tracing.py index 909f35ec1..b296e7794 100644 --- a/src/utils/otel_tracing.py +++ b/src/utils/otel_tracing.py @@ -118,6 +118,7 @@ def anonymize_value(value: str, max_length: int = 50) -> str: Raises: ValueError: If OTEL_ANONYMIZATION_SECRET environment variable is not set and OTEL SDK is enabled. + """ # Get HMAC secret from environment - fail clearly if not configured secret = os.environ.get(OTEL_ANONYMIZATION_SECRET_ENV_VAR) @@ -146,6 +147,7 @@ def set_span_attributes( Parameters: span: The OpenTelemetry span to set attributes on. attributes: Mapping of ``SpanAttributes`` keys to values. + """ for key, value in attributes.items(): span.set_attribute(key, value) @@ -161,6 +163,7 @@ def shield_span_attributes( Returns: Span attribute mapping with decision and, when blocked, reason. + """ attributes: dict[SpanAttributes, Any] = { SpanAttributes.SHIELD_RESULT: moderation_result.decision, @@ -186,6 +189,7 @@ def llm_inference_span_attributes( Returns: Span attribute mapping ready for ``set_span_attributes``. + """ return { SpanAttributes.RAG_CHUNKS: json.dumps( @@ -219,6 +223,7 @@ def root_span_turn_attributes( Returns: Span attribute mapping ready for ``set_span_attributes``. + """ attributes: dict[SpanAttributes, Any] = { SpanAttributes.OUTPUT: turn_summary.llm_response, @@ -238,6 +243,7 @@ def add_span_event( span: The OpenTelemetry span to add the event to. event_name: Name of the event. attributes: Optional dictionary of event attributes. + """ if attributes is None: attributes = {} @@ -255,6 +261,7 @@ def record_exception( span: The OpenTelemetry span to record the exception on. exception: The exception to record. attributes: Optional additional attributes for the exception event. + """ span_attributes = ( {str(key): value for key, value in attributes.items()} if attributes else None diff --git a/src/utils/prompts.py b/src/utils/prompts.py index 5822f7b19..56bd1e5da 100644 --- a/src/utils/prompts.py +++ b/src/utils/prompts.py @@ -39,6 +39,7 @@ def get_system_prompt( are disabled (disable_query_system_prompt) and a non-None `system_prompt` is provided; the response instructs the client to remove the relevant field from the request. + """ system_prompt_disabled = ( configuration.customization is not None @@ -87,6 +88,7 @@ def get_topic_summary_system_prompt() -> str: Returns: str: The topic summary system prompt from the active custom profile if set, otherwise the default prompt. + """ # profile takes precedence for setting prompt if ( diff --git a/src/utils/pydantic_ai_helpers.py b/src/utils/pydantic_ai_helpers.py index 57861cc99..ddbbf65cd 100644 --- a/src/utils/pydantic_ai_helpers.py +++ b/src/utils/pydantic_ai_helpers.py @@ -47,6 +47,7 @@ def _skills_capability( Returns: SkillsCapability when skill paths are configured, or None when skills are disabled. + """ if skills_config is None or not skills_config.paths: return None @@ -126,6 +127,7 @@ def get_skills_metadata( Returns: List of ``SkillMetadata`` with ``name`` and ``description`` for each loaded skill. + """ capability = _skills_capability(skills) if capability is None: @@ -146,6 +148,7 @@ def get_agent_capability_tools( Returns: Catalog tools for the ``/tools`` endpoint response format. + """ capabilities = _agent_capabilities(skills) or [] @@ -173,6 +176,7 @@ def _shield_capability(shield: ShieldConfiguration) -> AgentCapability[object]: Raises: ValueError: If ``shield.config`` doesn't match a known shield config type. + """ match shield.config: case QuestionValidityConfig(): @@ -203,6 +207,7 @@ def _agent_capabilities( Returns: Configured capabilities, or None when no capabilities are enabled. + """ capabilities: list[AgentCapability[object]] = [] for shield in shields or []: @@ -239,6 +244,7 @@ def captured_output_items(agent: Any) -> list[Any]: Returns: The captured output items, or an empty list when the model did not capture any (for example a non-OGX model in a test double). + """ model = getattr(agent, "model", None) items = getattr(model, "last_output_items", None) @@ -281,6 +287,7 @@ def build_agent( Returns: ``Agent`` configured for ``await agent.run(...)`` (or streaming) against the same stack configuration as ``client.responses.create(**responses_params.model_dump())``. + """ shield_configs = get_shields_for_request(config.shields, shields) capabilities = _agent_capabilities(config.skills, shield_configs, no_tools=no_tools) diff --git a/src/utils/query.py b/src/utils/query.py index 2cc911e66..b6df0f12f 100644 --- a/src/utils/query.py +++ b/src/utils/query.py @@ -51,6 +51,7 @@ def is_context_length_error(error_message: str) -> bool: Returns: True if the error indicates context length was exceeded. + """ msg_lower = error_message.lower() return "context_length" in msg_lower or "context length" in msg_lower @@ -79,6 +80,7 @@ def store_conversation_into_cache( operation must match the user id. topic_summary (Optional[str]): Optional topic summary to store alongside the conversation; ignored if None or empty. + """ if configuration.conversation_cache_configuration.type is None: logger.warning("Conversation cache is not configured") @@ -111,6 +113,7 @@ def validate_model_provider_override( Raises: HTTPException: HTTP 403 if the request includes model/provider override and the caller lacks Action.MODEL_OVERRIDE permission. + """ has_override = provider is not None or (model is not None and "/" in model) if has_override and Action.MODEL_OVERRIDE not in authorized_actions: @@ -135,6 +138,7 @@ def prepare_input( Returns: str: The input text with RAG context and text attachments appended (if any) + """ input_text: str = query_request.query if inline_rag_context: @@ -163,6 +167,7 @@ def build_multimodal_input( Returns: List of UserContent items: the text string followed by ImageUrl objects. + """ parts: list[UserContent] = [text] for attachment in image_attachments: @@ -205,6 +210,7 @@ def store_query_results( # pylint: disable=too-many-arguments Raises: HTTPException: On any database, cache, or IO errors during processing + """ provider_id, model_id = extract_provider_and_model_from_model_id(model) # Store transcript if enabled @@ -291,6 +297,7 @@ def consume_query_tokens( Raises: HTTPException: On database errors during token consumption + """ provider, model = extract_provider_and_model_from_model_id(model_id) try: @@ -315,6 +322,7 @@ def is_transcripts_enabled() -> bool: Returns: bool: True if transcripts is enabled, False otherwise. + """ transcripts_enabled: bool = ( configuration.user_data_collection_configuration.transcripts_enabled @@ -343,6 +351,7 @@ def persist_user_conversation_details( provider_id: The provider identifier topic_summary: Optional topic summary for the conversation response_id: Response ID for the conversation + """ # Normalize the conversation ID (strip 'conv_' prefix if present) normalized_id = normalize_conversation_id(conversation_id) @@ -431,6 +440,7 @@ def update_conversation_topic_summary( user_id: Optional user ID for cache update; when provided with cache configured, also updates the conversation cache. skip_userid_check: Whether to skip user ID validation for cache operations. + """ normalized_id = normalize_conversation_id(conversation_id) with get_session() as session: @@ -470,6 +480,7 @@ def validate_attachments_metadata(attachments: list[Attachment]) -> None: Raises: HTTPException: If any attachment has an invalid type or content type, an HTTP 422 error is raised. + """ for attachment in attachments: if attachment.attachment_type not in constants.ATTACHMENT_TYPES: @@ -502,6 +513,7 @@ def extract_provider_and_model_from_model_id(model_id: str) -> tuple[str, str]: Returns: tuple[str, str]: The provider and model. + """ split = model_id.split("/", 1) if len(split) == 2: @@ -531,6 +543,7 @@ def normalize_vertex_ai_model_id(model_id: str) -> str: Returns: Normalized model ID with Vertex AI prefix stripped if present + """ if model_id.startswith("publishers/google/models/"): return model_id.replace("publishers/google/models/", "google/", 1) @@ -550,6 +563,7 @@ def is_resource_exhausted_error(error_message: str) -> bool: Returns: True if the message indicates a wrapped RESOURCE_EXHAUSTED error. + """ return "resource_exhausted" in error_message.lower() @@ -565,6 +579,7 @@ def handle_known_apistatus_errors( Returns: AbstractErrorResponse: The error response model. + """ error_message = getattr(error, "message", str(error)) if is_context_length_error(error_message): diff --git a/src/utils/quota_utils.py b/src/utils/quota_utils.py index 84ae282a1..51846862c 100644 --- a/src/utils/quota_utils.py +++ b/src/utils/quota_utils.py @@ -46,6 +46,7 @@ def consume_tokens( Returns: ------- None + """ # record token usage history if token_usage_history is not None: @@ -81,6 +82,7 @@ def check_tokens_available(quota_limiters: list[QuotaLimiter], user_id: str) -> ------ HTTPException: With status 500 if database communication fails, or status 429 if quota is exceeded. + """ with tracer.start_as_current_span("quota.check") as span: try: @@ -118,6 +120,7 @@ def get_available_quotas( Raises: HTTPException: With status 500 if database communication fails. + """ available_quotas: dict[str, int] = {} diff --git a/src/utils/reranker.py b/src/utils/reranker.py index cb691fcbf..06953ac06 100644 --- a/src/utils/reranker.py +++ b/src/utils/reranker.py @@ -29,6 +29,7 @@ async def _get_cross_encoder(model_name: str) -> Any: Returns: Loaded CrossEncoder model instance, or None if loading fails. + """ # Check if reranking is enabled before attempting to load the model if ( @@ -73,6 +74,7 @@ async def rerank_chunks_with_cross_encoder( Returns: Top top_k chunks sorted by combined cross-encoder and weighted score (descending) + """ if not chunks: return [] @@ -192,6 +194,7 @@ def apply_byok_rerank_boost( Returns: Same chunks with BYOK scores boosted, sorted by score descending. + """ boosted = [] for chunk in chunks: diff --git a/src/utils/responses.py b/src/utils/responses.py index 852d10af4..2e94fd302 100644 --- a/src/utils/responses.py +++ b/src/utils/responses.py @@ -162,6 +162,7 @@ async def get_vector_store_ids( Raises: HTTPException: With ServiceUnavailableResponse if connection fails, or InternalServerErrorResponse if API returns an error status + """ if vector_store_ids is not None: return vector_store_ids @@ -192,6 +193,7 @@ async def get_topic_summary( # pylint: disable=too-many-nested-blocks Returns: The topic summary for the question + """ try: # Normalize Vertex AI model IDs to work around OGX 0.6.x bug @@ -236,6 +238,7 @@ async def maybe_get_topic_summary( Returns: Generated topic summary, or None when topic summaries are disabled. + """ if not generate_topic_summary: return None @@ -274,6 +277,7 @@ async def prepare_tools( # pylint: disable=too-many-arguments,too-many-position Returns: List of tool configurations, or None if no tools available + """ if no_tools: return None @@ -332,6 +336,7 @@ def _build_provider_data_headers( Returns: Dict with a single ``X-OGX-Provider-Data`` key, or None when no MCP tools carry headers. + """ if not tools: return None @@ -376,6 +381,7 @@ async def prepare_responses_params( # pylint: disable=too-many-arguments,too-ma Returns: ResponsesApiParams containing all prepared parameters for the API request + """ request_model = ( f"{query_request.provider}/{query_request.model}" @@ -468,6 +474,7 @@ def extract_vector_store_ids_from_tools( Returns: ------- List of vector store IDs used in file_search tools, or empty list. + """ if not tools: return [] @@ -490,6 +497,7 @@ def tool_matches_allowed_entry(tool: InputTool, entry: dict[str, str]) -> bool: ------- True if each entry key exists on the tool, the attribute is not None, and the value matches (including string coercion). + """ for key, value in entry.items(): if not hasattr(tool, key): @@ -519,6 +527,7 @@ def group_mcp_tools_by_server( ------- Mapping from server_label to None (no name restriction) or to the list of allowed tool names on that server. + """ unrestricted_servers: set[str] = set() server_to_names: dict[str, list[str]] = {} @@ -566,6 +575,7 @@ def mcp_strip_name_from_allowlist_entries( Returns: ------- Shallow-copied rows; name is dropped only when type is mcp. + """ result: list[dict[str, str]] = [] for entry in allowed_entries: @@ -592,6 +602,7 @@ def mcp_project_allowed_tools_to_names( ------- List of names in the intersection, or None if names is empty or the intersection is empty. + """ if not names: return None @@ -628,6 +639,7 @@ def filter_tools_by_allowed_entries( Sublist of tools matching at least one sanitized row. MCP tools may be copied with a tighter allowed_tools list when the allowlist names tools per server. Empty allowlist yields an empty list. + """ if tools is None: return None @@ -686,6 +698,7 @@ def resolve_vector_store_ids( Returns: ------- List of OGX vector_db_ids ready for the OGX API. + """ rag_id_to_vector_db_id = {brag.rag_id: brag.vector_db_id for brag in byok_rags} rag_id_to_vector_db_id[constants.OKP_RAG_ID] = ( @@ -708,6 +721,7 @@ def translate_tools_vector_store_ids( ------- New list of tools with file_search vector_store_ids translated; other tools unchanged. + """ result: list[InputTool] = [] for tool in tools: @@ -727,6 +741,7 @@ def get_rag_tools(vector_store_ids: list[str]) -> Optional[list[InputToolFileSea Returns: List containing file_search tool configuration, or empty list if no stores available + """ if vector_store_ids == []: return [] @@ -764,6 +779,7 @@ async def get_mcp_tools( Raises: HTTPException: 401 with WWW-Authenticate header when an MCP server uses OAuth, no headers are passed, and the server responds with 401 and WWW-Authenticate. + """ complete_headers = build_mcp_headers( configuration, mcp_headers or {}, request_headers, token @@ -838,6 +854,7 @@ def apply_mcp_headers_to_explicit_tools( Returns: New tool list with MCP entries updated or omitted. + """ if not tools: return tools @@ -904,6 +921,7 @@ def _build_okp_doc_url(attributes: dict[str, Any]) -> Optional[str]: Returns: Fully-qualified document URL, or None if no usable path is found. + """ offline = configuration.okp.offline if offline: @@ -933,6 +951,7 @@ def parse_referenced_documents( # pylint: disable=too-many-locals Returns: List of referenced documents with doc_url, doc_title, and source + """ documents: list[ReferencedDocument] = [] # Use a set to track unique documents by (doc_url, doc_title) tuple @@ -1002,6 +1021,7 @@ def parse_rag_chunks( Returns: List of RAG chunks derived from tool file search results (not mutated in place). + """ if response is None or not response.output: return [] @@ -1032,6 +1052,7 @@ def extract_token_usage( Returns: TokenCounter with input_tokens and output_tokens + """ provider_id, model_id = extract_provider_and_model_from_model_id(model) if usage is None: @@ -1072,6 +1093,7 @@ def build_tool_call_summary( # pylint: disable=too-many-return-statements,too-m Returns: Tuple of (ToolCallSummary, ToolResultSummary), one may be None + """ item_type = getattr(output_item, "type", None) @@ -1222,6 +1244,7 @@ def build_mcp_tool_call_from_arguments_done( Returns: ToolCallSummary for the MCP call, or None if item info not found + """ item_info = mcp_call_items.get(output_index) if not item_info: @@ -1249,6 +1272,7 @@ def build_tool_result_from_mcp_output_item_done( Returns: ToolResultSummary for the MCP call + """ content = output_item.error or (output_item.output or "") return ToolResultSummary( @@ -1279,6 +1303,7 @@ def resolve_source_for_result( Returns: ------- The resolved index name, or None if resolution is not possible. + """ if len(vector_store_ids) == 1: store_id = vector_store_ids[0] @@ -1308,6 +1333,7 @@ def _build_chunk_attributes(result: Any) -> Optional[dict[str, Any]]: Returns: ------- Dictionary of metadata attributes, or None if no attributes available. + """ attributes = getattr(result, "attributes", None) if not attributes: @@ -1331,6 +1357,7 @@ def extract_rag_chunks_from_file_search_item( Returns: List of RAG chunks extracted from the file search tool call item. + """ if item.results is None: return [] @@ -1359,6 +1386,7 @@ def parse_arguments_string(arguments_str: str) -> dict[str, Any]: Returns: Parsed dictionary if successful, otherwise {"args": arguments_str} + """ # Try parsing as-is first (most common case) try: @@ -1399,6 +1427,7 @@ async def check_model_configured( Raises: HTTPException: If there's a connection error or other API error + """ try: models = parse_model_list_response(await client.openai.list()) @@ -1445,6 +1474,7 @@ async def select_model_for_responses( Raises: HTTPException: If models cannot be fetched or an error occurs, or if no LLM model is found + """ if request_model: return request_model @@ -1508,6 +1538,7 @@ def is_server_deployed_output(output_item: ResponseOutput) -> bool: Returns: True if the item should be processed by LCS, False for client tools. + """ item_type = getattr(output_item, "type", None) @@ -1551,6 +1582,7 @@ def build_turn_summary( # pylint: disable=too-many-arguments,too-many-positiona TurnSummary with extracted response text, referenced_documents, rag_chunks, tool_calls, and tool_results. All fields are empty/default if response is None or has no output. + """ summary = TurnSummary() @@ -1590,6 +1622,7 @@ def extract_text_from_response_items( Returns: Extracted text content concatenated from all items, or empty string if None. + """ if response_items is None: return "" @@ -1611,6 +1644,7 @@ def extract_text_from_response_item(response_item: ResponseItem) -> str: Returns: Extracted text content, or empty string if not a message. + """ if response_item.type != "message": return "" @@ -1631,6 +1665,7 @@ def _extract_text_from_content( Returns: Extracted text content. Only extracts text from input_text, output_text, or refusal types. Other content types (images, files, etc.) are ignored. + """ if isinstance(content, str): return content @@ -1680,6 +1715,7 @@ async def create_new_conversation( Returns: The new conversation's ID (string), as returned by the API. + """ try: conversation = await client.conversations.create(metadata={}) @@ -1700,6 +1736,7 @@ def get_zero_usage() -> ResponseUsage: Returns: Usage object with zero values for input and output tokens. + """ return ResponseUsage( input_tokens=0, @@ -1718,6 +1755,7 @@ def extract_attachments_text(response_input: ResponseInput) -> str: Returns: All present file_data values joined by double newline. + """ if isinstance(response_input, str): return "" @@ -1757,6 +1795,7 @@ def _merge_tools( Raises: HTTPException: 409 if a client tool conflicts with a server-configured tool. + """ server_mcp_labels: set[str] = { t.server_label for t in server_tools if t.type == "mcp" @@ -1798,6 +1837,7 @@ async def _resolve_client_tools( Returns: Resolved list of tools. + """ # Per-request override of vector stores (user-facing rag_ids) vector_store_ids = extract_vector_store_ids_from_tools(tools) or None @@ -1837,6 +1877,7 @@ async def _resolve_server_tools( Returns: List of server-configured tools, or None if none are configured. + """ return await prepare_tools( vector_store_ids=None, # allow all vector stores configured @@ -1880,6 +1921,7 @@ def apply_reasoning_for_resolved_tools( Modified reasoning configuration with effort set to "none" if tools are present on a default-on reasoning model, or the original reasoning otherwise. + """ if not tools or not model_reasoning_enabled_by_default(model_id): return reasoning @@ -1922,6 +1964,7 @@ async def resolve_tool_choice( Returns: Prepared tools and resolved tool choice, each possibly None. + """ # If tool_choice mode is "none", tools are explicitly disallowed if isinstance(tool_choice, ToolChoiceMode) and tool_choice == ToolChoiceMode.none: @@ -1995,6 +2038,7 @@ async def resolve_client_tool_choice( Returns: Prepared tools and resolved tool choice, each possibly None. + """ # If tool_choice mode is "none", tools are explicitly disallowed if isinstance(tool_choice, ToolChoiceMode) and tool_choice == ToolChoiceMode.none: diff --git a/src/utils/rh_identity.py b/src/utils/rh_identity.py index e8fbf3b73..4056bb4b6 100644 --- a/src/utils/rh_identity.py +++ b/src/utils/rh_identity.py @@ -27,6 +27,7 @@ def get_rh_identity_context(request: Request) -> tuple[str, str]: Returns: Tuple of (org_id, system_id). Returns ("auth_disabled", "auth_disabled") when RH Identity auth is not configured or data is unavailable. + """ rh_identity: Optional[RHIdentityData] = getattr( request.state, "rh_identity_data", None diff --git a/src/utils/saved_prompts.py b/src/utils/saved_prompts.py index d7f12d3b8..909fe2bb7 100644 --- a/src/utils/saved_prompts.py +++ b/src/utils/saved_prompts.py @@ -51,6 +51,7 @@ def validate_saved_prompt_quota( Raises: SavedPromptLimitExceededError: If ``current_count >= max_prompts_per_user``. + """ if current_count >= max_prompts_per_user: raise SavedPromptLimitExceededError( @@ -79,6 +80,7 @@ def validate_saved_prompt_name( Raises: SavedPromptValidationError: If the name fails validation. + """ stripped_name = name.strip() if not stripped_name: @@ -108,6 +110,7 @@ def validate_saved_prompt_content( Raises: SavedPromptValidationError: If the content fails validation. + """ if not content.strip(): raise SavedPromptValidationError("Saved prompt content must not be empty") @@ -144,6 +147,7 @@ def create_saved_prompt( SavedPromptLimitExceededError: If the user is already at the limit. SavedPromptConflictError: If insert violates a unique constraint (practically always duplicate ``(user_id, name)``). + """ with get_session() as session: current_count = session.query(SavedPrompt).filter_by(user_id=user_id).count() @@ -187,6 +191,7 @@ def list_saved_prompts_by_user(user_id: str) -> list[SavedPrompt]: Returns: List of ``SavedPrompt`` rows for the user. Empty list if none exist. Tie order when ``created_at`` values are equal is database-defined. + """ with get_session() as session: return ( @@ -209,6 +214,7 @@ def delete_saved_prompt_by_id_and_user(prompt_id: str, user_id: str) -> None: SavedPromptNotFoundError: If no row exists for ``prompt_id``. SavedPromptAccessDeniedError: If the row exists but ``user_id`` does not match the owner. + """ with get_session() as session: saved_prompt = session.query(SavedPrompt).filter_by(id=prompt_id).first() diff --git a/src/utils/shields.py b/src/utils/shields.py index ec8b1522b..0834c8dda 100644 --- a/src/utils/shields.py +++ b/src/utils/shields.py @@ -68,6 +68,7 @@ def validate_shield_ids_override( Raises: ------ HTTPException: If shield_ids override is disabled but shield_ids is provided. + """ shield_ids_override_disabled = ( config.customization is not None @@ -102,6 +103,7 @@ async def run_shield_moderation_v2( Returns: Result indicating if content was blocked or passed. + """ with tracer.start_as_current_span("shield.moderate") as span: # Sanitize input before running any shields (OFFSEC-307 / LCORE-2749). @@ -160,6 +162,7 @@ def build_shield( Returns: The constructed safety capability. + """ match shield_config.config: case QuestionValidityConfig(): @@ -193,6 +196,7 @@ def get_shields_for_request( Raises: HTTPException: 404 if shield_ids is provided and any requested shield name is not present in shields. + """ if shield_ids is None: return list(shields) diff --git a/src/utils/stream_interrupts.py b/src/utils/stream_interrupts.py index 4c56f0410..8b947bf94 100644 --- a/src/utils/stream_interrupts.py +++ b/src/utils/stream_interrupts.py @@ -43,6 +43,7 @@ class ActiveStream: regardless of where the ``CancelledError`` lands. conversation_id: Conversation the streaming request belongs to, surfaced for observability of interrupt requests. + """ user_id: str @@ -89,6 +90,7 @@ def register_stream( is cancelled, executed in a separate task. conversation_id: Conversation the stream belongs to, retained for observability of subsequent interrupt requests. + """ with self._lock: self._streams[request_id] = ActiveStream( @@ -118,6 +120,7 @@ def cancel_stream(self, request_id: str, user_id: str) -> CancelStreamResult: Returns: ------- CancelStreamResult: Structured cancellation result. + """ on_interrupt = None with self._lock: @@ -147,6 +150,7 @@ def deregister_stream(self, request_id: str) -> None: Parameters: ---------- request_id: Unique streaming request identifier. + """ with self._lock: self._streams.pop(request_id, None) @@ -161,6 +165,7 @@ def get_stream(self, request_id: str) -> Optional[ActiveStream]: Returns: ------- Optional[ActiveStream]: Registered stream metadata, or None when absent. + """ with self._lock: return self._streams.get(request_id) @@ -181,6 +186,7 @@ def deregister_stream(request_id: str) -> None: Parameters: ---------- request_id: Unique streaming request identifier. + """ get_stream_interrupt_registry().deregister_stream(request_id) @@ -198,6 +204,7 @@ async def background_update_topic_summary( ---------- context: The response generator context. model: Model identifier used for topic summary generation. + """ try: topic_summary = await asyncio.wait_for( @@ -240,6 +247,7 @@ def build_interrupted_response(partial_tokens: list[str]) -> tuple[str, str]: A tuple of (full_response_text, suffix_to_emit) where full_response_text is the complete message for persistence and suffix_to_emit is the new content to send as a final SSE token event. + """ partial_text = "".join(partial_tokens) repaired_text = close_open_markdown(partial_text) @@ -276,6 +284,7 @@ async def persist_interrupted_turn( (the ``conversation`` parameter was dropped, and ``responses_params.input`` is the explicit rewrite); ``None`` otherwise (LCORE-1572). + """ try: if original_input is not None: @@ -369,6 +378,7 @@ def register_interrupt_callback( A mutable list ``[False]`` used as a persist-done guard; the caller should check ``guard[0]`` before persisting and set it to ``True`` afterwards. + """ guard: list[bool] = [False] diff --git a/src/utils/streaming_sse.py b/src/utils/streaming_sse.py index 3520c5ecf..c9dc98fcc 100644 --- a/src/utils/streaming_sse.py +++ b/src/utils/streaming_sse.py @@ -35,6 +35,7 @@ def stream_http_error_event( Returns: A Server-Sent Events (SSE) formatted error message containing the serialized error details. + """ logger.error("Error while obtaining answer for user question") media_type = media_type or MEDIA_TYPE_JSON @@ -61,6 +62,7 @@ def format_stream_data(d: dict[str, Any]) -> str: Returns: The formatted SSE data string. + """ data = json.dumps(d) return f"data: {data}\n\n" @@ -80,6 +82,7 @@ def stream_start_event(conversation_id: str, request_id: str) -> str: Returns: SSE-formatted string representing the start event. + """ return format_stream_data( { @@ -103,6 +106,7 @@ def stream_compaction_event(conversation_id: str) -> str: Returns: SSE-formatted string representing the compaction event. + """ return format_stream_data( { @@ -127,6 +131,7 @@ def stream_interrupted_event(request_id: str) -> str: Returns: SSE-formatted string representing the interrupted event. + """ return format_stream_data( { @@ -156,6 +161,7 @@ def stream_end_event( Returns: A Server-Sent Events (SSE) formatted string representing the end event. + """ if media_type == MEDIA_TYPE_TEXT: ref_docs_string = "\n".join( @@ -191,6 +197,7 @@ def stream_event(data: dict[str, Any], event_type: str, media_type: str) -> str: Returns: SSE-formatted string representing the event. + """ if media_type == MEDIA_TYPE_TEXT: if event_type == LLM_TOKEN_EVENT: @@ -223,6 +230,7 @@ def http_exception_stream_event(exc: HTTPException) -> str: Returns: SSE-formatted error event string. + """ detail = ( exc.detail if isinstance(exc.detail, dict) else {"response": str(exc.detail)} diff --git a/src/utils/suid.py b/src/utils/suid.py index 822f8b481..e9d1ac0a3 100644 --- a/src/utils/suid.py +++ b/src/utils/suid.py @@ -12,6 +12,7 @@ def get_suid() -> str: Returns: str: A UUID4 string suitable for use as a session identifier. + """ return str(uuid.uuid4()) @@ -37,6 +38,7 @@ def check_suid(suid: str) -> bool: 1. Standard UUID format (e.g., '550e8400-e29b-41d4-a716-446655440000') 2. 48-character hex string (OGX format) 3. 'conv_' prefix + 48-character hex string (53 chars total) + """ if not isinstance(suid, str): return False @@ -78,6 +80,7 @@ def normalize_conversation_id(conversation_id: str) -> str: 'abc123' >>> normalize_conversation_id('550e8400-e29b-41d4-a716-446655440000') '550e8400-e29b-41d4-a716-446655440000' + """ if conversation_id.startswith("conv_"): return conversation_id[5:] # Remove 'conv_' prefix @@ -100,6 +103,7 @@ def to_ogx_conversation_id(conversation_id: str) -> str: 'conv_abc123' >>> to_ogx_conversation_id('conv_abc123') 'conv_abc123' + """ if not conversation_id.startswith("conv_"): return f"conv_{conversation_id}" @@ -122,6 +126,7 @@ def check_suid_prompt(suid: str) -> bool: Returns: True if the string matches the prompt ID format; otherwise False. + """ prefix = "pmpt_" if not suid.startswith(prefix): diff --git a/src/utils/token_counter.py b/src/utils/token_counter.py index 3ff84815b..2d49c7a03 100644 --- a/src/utils/token_counter.py +++ b/src/utils/token_counter.py @@ -16,6 +16,7 @@ class TokenCounter: output_tokens: number of tokens received from LLM input_tokens_counted: number of input tokens counted by the handler llm_calls: number of LLM calls + """ input_tokens: int = 0 @@ -30,6 +31,7 @@ def __str__(self) -> str: Returns: summary (str): A formatted string containing `input_tokens`, `output_tokens`, `input_tokens_counted`, and `llm_calls`. + """ return ( f"{self.__class__.__name__}: " diff --git a/src/utils/token_estimator.py b/src/utils/token_estimator.py index 9992f6999..412271e97 100644 --- a/src/utils/token_estimator.py +++ b/src/utils/token_estimator.py @@ -49,6 +49,7 @@ def _get_encoding(encoding_name: str) -> tiktoken.Encoding: Raises: ValueError: Propagated from tiktoken when the encoding name is unknown. + """ return tiktoken.get_encoding(encoding_name) @@ -70,6 +71,7 @@ def estimate_tokens(text: str, encoding_name: str = DEFAULT_ENCODING_NAME) -> in Raises: ValueError: When *encoding_name* is not recognized by tiktoken. + """ if not text: return 0 @@ -90,6 +92,7 @@ def extract_message_text(message: Any) -> str: Returns: The textual content joined by spaces, or the empty string when no text can be located. + """ content = getattr(message, "content", None) if content is None: @@ -135,6 +138,7 @@ def estimate_conversation_tokens( Returns: The total token count. + """ encoding = _get_encoding(encoding_name) total = 0 @@ -166,5 +170,6 @@ def get_context_window( Returns: The context window size in tokens, or ``None`` when unset. + """ return inference_config.context_windows.get(model) diff --git a/src/utils/tool_formatter.py b/src/utils/tool_formatter.py index 5181f5ccf..15d7e3dc7 100644 --- a/src/utils/tool_formatter.py +++ b/src/utils/tool_formatter.py @@ -25,6 +25,7 @@ def input_schema_to_parameters( Returns: Flat parameter models for the tools endpoint response. + """ if not schema or "properties" not in schema: return [] @@ -65,6 +66,7 @@ def extract_clean_description(description: str) -> str: Returns: ------- Clean description without metadata + """ min_description_length = 20 fallback_truncation_length = 200 @@ -130,6 +132,7 @@ def build_catalog_tool( Returns: Typed catalog tool entry. + """ cleaned_description = tool.description or "" if cleaned_description and ( @@ -163,6 +166,7 @@ def translate_vector_store_ids_to_user_facing( ------- list[dict[str, Any]]: New list of tool dicts; file_search entries get updated vector_store_ids. + """ if not rag_id_mapping: return list(tools) diff --git a/src/utils/types.py b/src/utils/types.py index 874bcbdba..44b49700b 100644 --- a/src/utils/types.py +++ b/src/utils/types.py @@ -23,6 +23,7 @@ def __call__(cls: type[T], *args: object, **kwargs: object) -> T: Returns: The singleton instance for this class. + """ if cls not in Singleton._instances: Singleton._instances[cls] = type.__call__(cls, *args, **kwargs) diff --git a/src/utils/vector_search.py b/src/utils/vector_search.py index cb477666d..e5fd479ed 100644 --- a/src/utils/vector_search.py +++ b/src/utils/vector_search.py @@ -52,6 +52,7 @@ def _filter_documents_for_chunks( Returns: Filtered list of documents that correspond to the final chunks. + """ # Create a set of unique identifiers from final chunks final_chunk_identifiers = set() @@ -90,6 +91,7 @@ def _get_okp_base_url() -> AnyUrl: Returns: Parsed base URL as ``AnyUrl``. + """ rhokp = configuration.okp.rhokp_url if rhokp is None: @@ -130,6 +132,7 @@ def _build_query_params( Returns: Parameter dictionary for ``vector_io.query`` with extracted filters at top level. + """ resolved_mode = ( solr.mode @@ -188,6 +191,7 @@ def _extract_byok_rag_chunks( Returns: List of result dictionaries with weighted scores + """ result_chunks = [] for chunk, score in zip( @@ -232,6 +236,7 @@ def _format_rag_context(rag_chunks: list[RAGChunk], query: str) -> str: Returns: Formatted string with RAG context metadata attributes + """ if not rag_chunks: return "" @@ -285,6 +290,7 @@ async def _query_store_for_byok_rag( # pylint: disable=too-many-arguments,too-m Returns: List of weighted result dictionaries, or empty list on error + """ try: search_response = await client.vector_io.query( @@ -346,6 +352,7 @@ def _process_byok_rag_chunks_for_documents( Returns: List of referenced documents extracted from BYOK RAG chunks + """ referenced_documents = [] seen_doc_ids = set() @@ -415,6 +422,7 @@ def _process_solr_chunks_for_documents( Returns: List of referenced documents extracted from Solr chunks + """ doc_ids_from_chunks = [] metadata_doc_ids = set() @@ -480,6 +488,7 @@ async def _fetch_byok_rag( # pylint: disable=too-many-locals Tuple containing: - rag_chunks: RAG chunks from BYOK RAG - referenced_documents: Documents referenced in BYOK RAG results + """ limit = configuration.rag.byok.max_chunks rag_chunks: list[RAGChunk] = [] @@ -591,6 +600,7 @@ async def _fetch_okp_rag( # pylint: disable=too-many-locals Tuple containing: - rag_chunks: RAG chunks from Solr - referenced_documents: Documents referenced in Solr results + """ rag_chunks: list[RAGChunk] = [] referenced_documents: list[ReferencedDocument] = [] @@ -665,6 +675,7 @@ async def build_rag_context( # pylint: disable=too-many-locals,too-many-branche Returns: RAGContext containing formatted context text and referenced documents + """ with tracer.start_as_current_span("rag.retrieve") as span: # Set RAG input attribute @@ -749,6 +760,7 @@ def _join_okp_doc_url(base_url: AnyUrl, reference: Optional[str]) -> str: Returns: Well-formed doc_url string, or empty string if reference is empty. + """ if not reference: return "" @@ -773,6 +785,7 @@ def _build_document_url( Tuple of (doc_url, reference_doc) where: - doc_url: The full URL for the document - reference_doc: The document reference used for deduplication + """ base_url = _get_okp_base_url() if offline: @@ -798,6 +811,7 @@ def _convert_solr_chunks_to_rag_format( Returns: List of RAGChunk objects + """ rag_chunks = [] @@ -862,6 +876,7 @@ def append_inline_rag_context_to_responses_input( Returns: ------- The same type as input_value, with context merged in. + """ if not inline_rag_context_text: return input_value diff --git a/tests/e2e/features/steps/a2a.py b/tests/e2e/features/steps/a2a.py index 6c04c03d9..5b5f00035 100644 --- a/tests/e2e/features/steps/a2a.py +++ b/tests/e2e/features/steps/a2a.py @@ -75,6 +75,7 @@ def _consume_sse_jsonrpc_stream( A tuple of (parsed JSON-RPC ``result`` objects, bounded diagnostic excerpt of the raw stream, first JSON-RPC ``error`` object seen, if any). The caller decides whether/when to raise on that error. + """ results: list[dict[str, Any]] = [] excerpt = bytearray()