Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,8 @@ class Action(str, Enum):
INFO = "info"
# Allow overriding model/provider via request
MODEL_OVERRIDE = "model_override"
# RHEL Lightspeed rlsapi v1 compatibility - stateless inference (no history/RAG)
RLSAPI_V1_INFER = "rlsapi_v1_infer"


class AccessRule(ConfigurationBase):
Expand Down
1 change: 1 addition & 0 deletions src/models/rlsapi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Pydantic models for rlsapi v1 integration."""
176 changes: 176 additions & 0 deletions src/models/rlsapi/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""Models for rlsapi v1 REST API requests."""

from pydantic import Field, field_validator

from models.config import ConfigurationBase


class RlsapiV1Attachment(ConfigurationBase):
"""Attachment data from rlsapi v1 context.

Attributes:
contents: The textual contents of the file read on the client machine.
mimetype: The MIME type of the file.
"""

contents: str = Field(
default="",
description="File contents read on client",
examples=["# Configuration file\nkey=value"],
)
mimetype: str = Field(
default="",
description="MIME type of the file",
examples=["text/plain", "application/json"],
)


class RlsapiV1Terminal(ConfigurationBase):
"""Terminal output from rlsapi v1 context.

Attributes:
output: The textual contents of the terminal read on the client machine.
"""

output: str = Field(
default="",
description="Terminal output from client",
examples=["bash: command not found", "Permission denied"],
)


class RlsapiV1SystemInfo(ConfigurationBase):
"""System information from rlsapi v1 context.

Attributes:
os: The operating system of the client machine.
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(default="", description="Operating system name", examples=["RHEL"])
version: str = Field(
default="", description="Operating system version", examples=["9.3", "8.10"]
)
arch: str = Field(
default="", description="System architecture", examples=["x86_64", "aarch64"]
)
system_id: str = Field(
default="",
alias="id",
description="Client machine ID",
examples=["01JDKR8N7QW9ZMXVGK3PB5TQWZ"],
)

model_config = {"populate_by_name": True}


class RlsapiV1CLA(ConfigurationBase):
"""Command Line Assistant information from rlsapi v1 context.

Attributes:
nevra: The NEVRA (Name-Epoch-Version-Release-Architecture) of the CLA.
version: The version of the command line assistant.
"""

nevra: str = Field(
default="",
description="CLA NEVRA identifier",
examples=["command-line-assistant-0:0.2.0-1.el9.noarch"],
)
version: str = Field(
default="",
description="Command line assistant version",
examples=["0.2.0"],
)


class RlsapiV1Context(ConfigurationBase):
"""Context data for rlsapi v1 /infer request.

Attributes:
stdin: Redirect input read by command-line-assistant.
attachments: Attachment object received by the client.
terminal: Terminal object received by the client.
systeminfo: System information object received by the client.
cla: Command Line Assistant information.
"""

stdin: str = Field(
default="",
description="Redirect input from stdin",
examples=["piped input from previous command"],
)
attachments: RlsapiV1Attachment = Field(
default_factory=RlsapiV1Attachment,
description="File attachment data",
)
terminal: RlsapiV1Terminal = Field(
default_factory=RlsapiV1Terminal,
description="Terminal output context",
)
systeminfo: RlsapiV1SystemInfo = Field(
default_factory=RlsapiV1SystemInfo,
description="Client system information",
)
cla: RlsapiV1CLA = Field(
default_factory=RlsapiV1CLA,
description="Command line assistant metadata",
)


class RlsapiV1InferRequest(ConfigurationBase):
"""RHEL Lightspeed rlsapi v1 /infer request.

Attributes:
question: User question string.
context: Context with system info, terminal output, etc. (defaults provided).
skip_rag: Whether to skip RAG retrieval (default False).

Example:
```python
request = RlsapiV1InferRequest(
question="How do I list files?",
context=RlsapiV1Context(
systeminfo=RlsapiV1SystemInfo(os="RHEL", version="9.3"),
terminal=RlsapiV1Terminal(output="bash: command not found"),
),
)
```
"""

question: str = Field(
...,
min_length=1,
description="User question",
examples=["How do I list files?", "How do I configure SELinux?"],
)
context: RlsapiV1Context = Field(
default_factory=RlsapiV1Context,
description="Optional context (system info, terminal output, stdin, attachments)",
)
skip_rag: bool = Field(
default=False,
description="Whether to skip RAG retrieval",
examples=[False, True],
)

@field_validator("question")
@classmethod
def validate_question(cls, value: str) -> str:
"""Validate question is not empty or whitespace-only.

Args:
value: The question string to validate.

Returns:
The stripped question string.

Raises:
ValueError: If the question is empty or whitespace-only.
"""
stripped = value.strip()
if not stripped:
raise ValueError("Question cannot be empty or whitespace-only")
return stripped
53 changes: 53 additions & 0 deletions src/models/rlsapi/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Models for rlsapi v1 REST API responses."""

from pydantic import Field

from models.config import ConfigurationBase
from models.responses import AbstractSuccessfulResponse


class RlsapiV1InferData(ConfigurationBase):
"""Response data for rlsapi v1 /infer endpoint.

Attributes:
text: The generated response text.
request_id: Unique identifier for the request.
"""

text: str = Field(
...,
description="Generated response text",
examples=["To list files in Linux, use the `ls` command."],
)
request_id: str | None = Field(
None,
description="Unique request identifier",
examples=["01JDKR8N7QW9ZMXVGK3PB5TQWZ"],
)


class RlsapiV1InferResponse(AbstractSuccessfulResponse):
"""RHEL Lightspeed rlsapi v1 /infer response.

Attributes:
data: Response data containing text and request_id.
"""

data: RlsapiV1InferData = Field(
...,
description="Response data containing text and request_id",
)

model_config = {
"extra": "forbid",
"json_schema_extra": {
"examples": [
{
"data": {
"text": "To list files in Linux, use the `ls` command.",
"request_id": "01JDKR8N7QW9ZMXVGK3PB5TQWZ",
}
}
]
},
}
10 changes: 7 additions & 3 deletions tests/unit/authentication/test_api_key_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

"""Unit tests for functions defined in authentication/api_key_token.py"""

from fastapi import Request, HTTPException
import pytest
from fastapi import HTTPException, Request
from pydantic import SecretStr

from authentication.api_key_token import APIKeyTokenAuthDependency
Expand Down Expand Up @@ -71,7 +71,9 @@ async def test_api_key_with_token_auth_dependency_no_token(
await dependency(request)

assert exc_info.value.status_code == 401
assert exc_info.value.detail["cause"] == "No Authorization header found"
detail = exc_info.value.detail
assert isinstance(detail, dict)
assert detail["cause"] == "No Authorization header found"


async def test_api_key_with_token_auth_dependency_no_bearer(
Expand All @@ -94,7 +96,9 @@ async def test_api_key_with_token_auth_dependency_no_bearer(
await dependency(request)

assert exc_info.value.status_code == 401
assert exc_info.value.detail["cause"] == "No token found in Authorization header"
detail = exc_info.value.detail
assert isinstance(detail, dict)
assert detail["cause"] == "No token found in Authorization header"


async def test_api_key_with_token_auth_dependency_invalid(
Expand Down
1 change: 1 addition & 0 deletions tests/unit/models/rlsapi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Unit tests for rlsapi v1 models."""
Loading
Loading