Skip to content

Commit e897ab0

Browse files
authored
Merge pull request #896 from major/rlsapi-models
Add rlsapi v1 infer request/response models
2 parents 69d27b9 + bedc67c commit e897ab0

8 files changed

Lines changed: 707 additions & 3 deletions

File tree

src/models/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,8 @@ class Action(str, Enum):
804804
INFO = "info"
805805
# Allow overriding model/provider via request
806806
MODEL_OVERRIDE = "model_override"
807+
# RHEL Lightspeed rlsapi v1 compatibility - stateless inference (no history/RAG)
808+
RLSAPI_V1_INFER = "rlsapi_v1_infer"
807809

808810

809811
class AccessRule(ConfigurationBase):

src/models/rlsapi/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Pydantic models for rlsapi v1 integration."""

src/models/rlsapi/requests.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"""Models for rlsapi v1 REST API requests."""
2+
3+
from pydantic import Field, field_validator
4+
5+
from models.config import ConfigurationBase
6+
7+
8+
class RlsapiV1Attachment(ConfigurationBase):
9+
"""Attachment data from rlsapi v1 context.
10+
11+
Attributes:
12+
contents: The textual contents of the file read on the client machine.
13+
mimetype: The MIME type of the file.
14+
"""
15+
16+
contents: str = Field(
17+
default="",
18+
description="File contents read on client",
19+
examples=["# Configuration file\nkey=value"],
20+
)
21+
mimetype: str = Field(
22+
default="",
23+
description="MIME type of the file",
24+
examples=["text/plain", "application/json"],
25+
)
26+
27+
28+
class RlsapiV1Terminal(ConfigurationBase):
29+
"""Terminal output from rlsapi v1 context.
30+
31+
Attributes:
32+
output: The textual contents of the terminal read on the client machine.
33+
"""
34+
35+
output: str = Field(
36+
default="",
37+
description="Terminal output from client",
38+
examples=["bash: command not found", "Permission denied"],
39+
)
40+
41+
42+
class RlsapiV1SystemInfo(ConfigurationBase):
43+
"""System information from rlsapi v1 context.
44+
45+
Attributes:
46+
os: The operating system of the client machine.
47+
version: The version of the operating system.
48+
arch: The architecture of the client machine.
49+
system_id: The id of the client machine.
50+
"""
51+
52+
os: str = Field(default="", description="Operating system name", examples=["RHEL"])
53+
version: str = Field(
54+
default="", description="Operating system version", examples=["9.3", "8.10"]
55+
)
56+
arch: str = Field(
57+
default="", description="System architecture", examples=["x86_64", "aarch64"]
58+
)
59+
system_id: str = Field(
60+
default="",
61+
alias="id",
62+
description="Client machine ID",
63+
examples=["01JDKR8N7QW9ZMXVGK3PB5TQWZ"],
64+
)
65+
66+
model_config = {"populate_by_name": True}
67+
68+
69+
class RlsapiV1CLA(ConfigurationBase):
70+
"""Command Line Assistant information from rlsapi v1 context.
71+
72+
Attributes:
73+
nevra: The NEVRA (Name-Epoch-Version-Release-Architecture) of the CLA.
74+
version: The version of the command line assistant.
75+
"""
76+
77+
nevra: str = Field(
78+
default="",
79+
description="CLA NEVRA identifier",
80+
examples=["command-line-assistant-0:0.2.0-1.el9.noarch"],
81+
)
82+
version: str = Field(
83+
default="",
84+
description="Command line assistant version",
85+
examples=["0.2.0"],
86+
)
87+
88+
89+
class RlsapiV1Context(ConfigurationBase):
90+
"""Context data for rlsapi v1 /infer request.
91+
92+
Attributes:
93+
stdin: Redirect input read by command-line-assistant.
94+
attachments: Attachment object received by the client.
95+
terminal: Terminal object received by the client.
96+
systeminfo: System information object received by the client.
97+
cla: Command Line Assistant information.
98+
"""
99+
100+
stdin: str = Field(
101+
default="",
102+
description="Redirect input from stdin",
103+
examples=["piped input from previous command"],
104+
)
105+
attachments: RlsapiV1Attachment = Field(
106+
default_factory=RlsapiV1Attachment,
107+
description="File attachment data",
108+
)
109+
terminal: RlsapiV1Terminal = Field(
110+
default_factory=RlsapiV1Terminal,
111+
description="Terminal output context",
112+
)
113+
systeminfo: RlsapiV1SystemInfo = Field(
114+
default_factory=RlsapiV1SystemInfo,
115+
description="Client system information",
116+
)
117+
cla: RlsapiV1CLA = Field(
118+
default_factory=RlsapiV1CLA,
119+
description="Command line assistant metadata",
120+
)
121+
122+
123+
class RlsapiV1InferRequest(ConfigurationBase):
124+
"""RHEL Lightspeed rlsapi v1 /infer request.
125+
126+
Attributes:
127+
question: User question string.
128+
context: Context with system info, terminal output, etc. (defaults provided).
129+
skip_rag: Whether to skip RAG retrieval (default False).
130+
131+
Example:
132+
```python
133+
request = RlsapiV1InferRequest(
134+
question="How do I list files?",
135+
context=RlsapiV1Context(
136+
systeminfo=RlsapiV1SystemInfo(os="RHEL", version="9.3"),
137+
terminal=RlsapiV1Terminal(output="bash: command not found"),
138+
),
139+
)
140+
```
141+
"""
142+
143+
question: str = Field(
144+
...,
145+
min_length=1,
146+
description="User question",
147+
examples=["How do I list files?", "How do I configure SELinux?"],
148+
)
149+
context: RlsapiV1Context = Field(
150+
default_factory=RlsapiV1Context,
151+
description="Optional context (system info, terminal output, stdin, attachments)",
152+
)
153+
skip_rag: bool = Field(
154+
default=False,
155+
description="Whether to skip RAG retrieval",
156+
examples=[False, True],
157+
)
158+
159+
@field_validator("question")
160+
@classmethod
161+
def validate_question(cls, value: str) -> str:
162+
"""Validate question is not empty or whitespace-only.
163+
164+
Args:
165+
value: The question string to validate.
166+
167+
Returns:
168+
The stripped question string.
169+
170+
Raises:
171+
ValueError: If the question is empty or whitespace-only.
172+
"""
173+
stripped = value.strip()
174+
if not stripped:
175+
raise ValueError("Question cannot be empty or whitespace-only")
176+
return stripped

src/models/rlsapi/responses.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Models for rlsapi v1 REST API responses."""
2+
3+
from pydantic import Field
4+
5+
from models.config import ConfigurationBase
6+
from models.responses import AbstractSuccessfulResponse
7+
8+
9+
class RlsapiV1InferData(ConfigurationBase):
10+
"""Response data for rlsapi v1 /infer endpoint.
11+
12+
Attributes:
13+
text: The generated response text.
14+
request_id: Unique identifier for the request.
15+
"""
16+
17+
text: str = Field(
18+
...,
19+
description="Generated response text",
20+
examples=["To list files in Linux, use the `ls` command."],
21+
)
22+
request_id: str | None = Field(
23+
None,
24+
description="Unique request identifier",
25+
examples=["01JDKR8N7QW9ZMXVGK3PB5TQWZ"],
26+
)
27+
28+
29+
class RlsapiV1InferResponse(AbstractSuccessfulResponse):
30+
"""RHEL Lightspeed rlsapi v1 /infer response.
31+
32+
Attributes:
33+
data: Response data containing text and request_id.
34+
"""
35+
36+
data: RlsapiV1InferData = Field(
37+
...,
38+
description="Response data containing text and request_id",
39+
)
40+
41+
model_config = {
42+
"extra": "forbid",
43+
"json_schema_extra": {
44+
"examples": [
45+
{
46+
"data": {
47+
"text": "To list files in Linux, use the `ls` command.",
48+
"request_id": "01JDKR8N7QW9ZMXVGK3PB5TQWZ",
49+
}
50+
}
51+
]
52+
},
53+
}

tests/unit/authentication/test_api_key_token.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

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

5-
from fastapi import Request, HTTPException
65
import pytest
6+
from fastapi import HTTPException, Request
77
from pydantic import SecretStr
88

99
from authentication.api_key_token import APIKeyTokenAuthDependency
@@ -71,7 +71,9 @@ async def test_api_key_with_token_auth_dependency_no_token(
7171
await dependency(request)
7272

7373
assert exc_info.value.status_code == 401
74-
assert exc_info.value.detail["cause"] == "No Authorization header found"
74+
detail = exc_info.value.detail
75+
assert isinstance(detail, dict)
76+
assert detail["cause"] == "No Authorization header found"
7577

7678

7779
async def test_api_key_with_token_auth_dependency_no_bearer(
@@ -94,7 +96,9 @@ async def test_api_key_with_token_auth_dependency_no_bearer(
9496
await dependency(request)
9597

9698
assert exc_info.value.status_code == 401
97-
assert exc_info.value.detail["cause"] == "No token found in Authorization header"
99+
detail = exc_info.value.detail
100+
assert isinstance(detail, dict)
101+
assert detail["cause"] == "No token found in Authorization header"
98102

99103

100104
async def test_api_key_with_token_auth_dependency_invalid(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Unit tests for rlsapi v1 models."""

0 commit comments

Comments
 (0)