Skip to content

Commit 4b40193

Browse files
authored
implement openapi spec x_ai_description (#1819)
implement openapi spec x_ai_description remove /check and /check/status endpoints from openapi spec document as not relevant, the endpoints still served/working, but should be removed from any resources, instead /api/v1/health and /api/v1/health/status should be used. Jira Issue: https://issues.redhat.com/browse/AAP-59302 Signed-off-by: Djebran Lezzoum <ldjebran@gmail.com>
1 parent 193a036 commit 4b40193

File tree

11 files changed

+335
-264
lines changed

11 files changed

+335
-264
lines changed

ansible_ai_connect/ai/api/openapi.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
INTERNAL_PATHS = ["/api/v1/service-index", "/check"]
16+
1517

1618
def preprocessing_filter_spec(endpoints):
1719
filtered = []
20+
1821
for path, path_regex, method, callback in endpoints:
19-
# do not add internal endpoints to schema
20-
if not path.startswith("/api/v1/service-index"):
21-
filtered.append((path, path_regex, method, callback))
22+
if any(path.startswith(internal_path) for internal_path in INTERNAL_PATHS):
23+
# do not add internal endpoints to schema
24+
continue
25+
filtered.append((path, path_regex, method, callback))
2226
return filtered

ansible_ai_connect/ai/api/views.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from string import Template
2020

2121
from ansible_anonymizer import anonymizer
22+
from ansible_base.lib.utils.schema import extend_schema_if_available
2223
from django.apps import apps
2324
from django.conf import settings
2425
from django.http import StreamingHttpResponse
@@ -833,6 +834,9 @@ class Explanation(AACSAPIView):
833834
},
834835
summary="Playbook explanation",
835836
)
837+
@extend_schema_if_available(
838+
extensions={"x-ai-description": "Create an explanation of a playbook"},
839+
)
836840
def post(self, request) -> Response:
837841
self.event.playbook_length = len(self.validated_data["content"])
838842
self.event.explanationId = self.validated_data["explanationId"]
@@ -890,6 +894,7 @@ class ExplanationRole(AACSAPIView):
890894
},
891895
summary="Role explanation",
892896
)
897+
@extend_schema_if_available(extensions={"x-ai-description": "Create an explanation of a role"})
893898
def post(self, request) -> Response:
894899
llm: ModelPipelineRoleExplanation = apps.get_app_config("ai").get_model_pipeline(
895900
ModelPipelineRoleExplanation
@@ -945,6 +950,9 @@ class GenerationPlaybook(AACSAPIView):
945950
},
946951
summary="Playbook generation",
947952
)
953+
@extend_schema_if_available(
954+
extensions={"x-ai-description": "Generate a playbook based on a text input"}
955+
)
948956
def post(self, request) -> Response:
949957
self.event.create_outline = self.validated_data["createOutline"]
950958
self.event.generationId = self.validated_data["generationId"]
@@ -1013,6 +1021,9 @@ class GenerationRole(AACSAPIView):
10131021
},
10141022
summary="Role generation",
10151023
)
1024+
@extend_schema_if_available(
1025+
extensions={"x-ai-description": "Generate a role based on a text input"}
1026+
)
10161027
def post(self, request) -> Response:
10171028
self.event.create_outline = self.validated_data["createOutline"]
10181029
self.event.generationId = self.validated_data["generationId"]

ansible_ai_connect/healthcheck/views.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import logging
1717
from datetime import datetime
1818

19+
from ansible_base.lib.utils.schema import extend_schema_if_available
1920
from django.conf import settings
2021
from django.http import HttpResponse, JsonResponse
2122
from django.utils.decorators import method_decorator
@@ -136,6 +137,9 @@ def __init__(self):
136137
methods=["GET"],
137138
summary="Health check with backend server status",
138139
)
140+
@extend_schema_if_available(
141+
extensions={"x-ai-description": "Retrieve health status with backend servers status"},
142+
)
139143
def get(self, request, *args, **kwargs):
140144
res = self.customView.get(request, *args, **kwargs)
141145
# res contains status_code = 200 for utilizing view cache. We need to set the correct
@@ -160,6 +164,9 @@ class WisdomServiceLivenessProbeView(APIView):
160164
summary="Liveness probe",
161165
)
162166
@method_decorator(never_cache)
167+
@extend_schema_if_available(
168+
extensions={"x-ai-description": "Retrieve health status"},
169+
)
163170
def get(self, request, *args, **kwargs):
164171
data = common_data()
165172
if settings.DEPLOYMENT_MODE == "onprem":
@@ -192,6 +199,9 @@ def normalise_status(status: str | dict[str, str]) -> str:
192199
summary="Chatbot health check",
193200
)
194201
@method_decorator(cache_page(CACHE_TIMEOUT))
202+
@extend_schema_if_available(
203+
extensions={"x-ai-description": "Retrieve chatbot health status"},
204+
)
195205
def get(self, request, *args, **kwargs):
196206
cb: ModelPipelineHealthCheck = ModelPipelineHealthCheck(pipeline_type=ModelPipelineChatBot)
197207
cb_streaming: ModelPipelineHealthCheck = ModelPipelineHealthCheck(

ansible_ai_connect/main/settings/development.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,17 @@
4040
"TAGS": [
4141
{"name": "ai", "description": "AI-related operations"},
4242
{"name": "me", "description": "Authenticated user information"},
43-
{"name": "check", "description": "Health check"},
4443
{"name": "wca", "description": "watsonx Code Assistant"},
4544
],
4645
"SCHEMA_PATH_PREFIX": r"/api/v[0-9]+",
4746
"SCHEMA_PATH_PREFIX_TRIM": True,
48-
"PREPROCESSING_HOOKS": ["ansible_ai_connect.ai.api.openapi.preprocessing_filter_spec"],
47+
"PREPROCESSING_HOOKS": [
48+
"ansible_ai_connect.ai.api.openapi.preprocessing_filter_spec",
49+
"ansible_base.api_documentation.preprocessing_hooks.collect_ai_description_metadata",
50+
],
51+
"POSTPROCESSING_HOOKS": [
52+
"ansible_base.api_documentation.postprocessing_hooks.add_x_ai_description",
53+
],
4954
}
5055

5156
# social_django does not process auth exceptions when DEBUG=True by default.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env python3
2+
# Copyright Red Hat
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import json
17+
import os
18+
19+
import yaml
20+
from django.test import TestCase
21+
22+
_current_dir_path = os.path.dirname(__file__)
23+
24+
X_AI_DESCRIPTION = "x-ai-description"
25+
X_AI_DESCRIPTION_MIN_LENGTH = 0
26+
X_AI_DESCRIPTION_MAX_LENGTH = 300
27+
28+
29+
class XAIDescription(TestCase):
30+
openapi_schema_path = os.path.join(
31+
_current_dir_path, "..", "..", "..", "tools", "openapi-schema"
32+
)
33+
34+
def setUp(self):
35+
self.openapi_schema_path_yaml = os.path.join(
36+
self.openapi_schema_path, "ansible-ai-connect-service.yaml"
37+
)
38+
self.openapi_schema_path_json = os.path.join(
39+
self.openapi_schema_path, "ansible-ai-connect-service.json"
40+
)
41+
42+
def assert_schema_x_ai_description(self, schema):
43+
schema_paths = schema.get("paths", None)
44+
self.assertIsNotNone(schema_paths)
45+
self.assertTrue(len(schema_paths) > 0)
46+
47+
operations_count = 0
48+
for path, operations in schema_paths.items():
49+
for method, operation in operations.items():
50+
if isinstance(operation, dict):
51+
self.assertIn(X_AI_DESCRIPTION, operation)
52+
x_ai_description = operation.get(X_AI_DESCRIPTION)
53+
self.assertIsInstance(x_ai_description, str)
54+
self.assertTrue(
55+
X_AI_DESCRIPTION_MIN_LENGTH
56+
< len(x_ai_description)
57+
< X_AI_DESCRIPTION_MAX_LENGTH,
58+
f"wrong string length ({len(x_ai_description)}) for: '{x_ai_description}', "
59+
f"minimum allowed: {X_AI_DESCRIPTION_MIN_LENGTH}, "
60+
f"maximum allowed: {X_AI_DESCRIPTION_MAX_LENGTH} ",
61+
)
62+
operations_count += 1
63+
64+
assert operations_count > 0
65+
66+
def test_openapi_schema_yaml(self):
67+
self.assertTrue(os.path.exists(self.openapi_schema_path_yaml))
68+
with open(os.path.join(self.openapi_schema_path_yaml)) as f:
69+
schema = yaml.safe_load(f)
70+
self.assert_schema_x_ai_description(schema)
71+
72+
def test_openapi_schema_json(self):
73+
self.assertTrue(os.path.exists(self.openapi_schema_path_json))
74+
with open(os.path.join(self.openapi_schema_path_json)) as f:
75+
schema = json.load(f)
76+
self.assert_schema_x_ai_description(schema)

ansible_ai_connect/users/views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import logging
1616

17+
from ansible_base.lib.utils.schema import extend_schema_if_available
1718
from django.apps import apps
1819
from django.conf import settings
1920
from django.contrib.auth.models import AnonymousUser
@@ -137,6 +138,9 @@ class MeRateThrottle(UserRateThrottle):
137138
throttle_classes = [MeRateThrottle]
138139

139140
@method_decorator(cache_per_user(ME_USER_CACHE_TIMEOUT_SEC))
141+
@extend_schema_if_available(
142+
extensions={"x-ai-description": "Retrieve current user information"}
143+
)
140144
def get(self, request, *args, **kwargs):
141145
return self.retrieve(request, *args, **kwargs)
142146

@@ -167,6 +171,7 @@ class MeRateThrottle(UserRateThrottle):
167171
throttle_classes = [MeRateThrottle]
168172

169173
@method_decorator(cache_per_user(ME_USER_CACHE_TIMEOUT_SEC))
174+
@extend_schema_if_available(extensions={"x-ai-description": "Retrieve current logged in user"})
170175
def get(self, request, *args, **kwargs):
171176
return self.retrieve(request, *args, **kwargs)
172177

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ dependencies = [
2424
'django_prometheus~=2.2.0',
2525
'django-test-migrations~=1.3.0',
2626
'djangorestframework~=3.15.2',
27-
'drf-spectacular~=0.27.2',
27+
'drf-spectacular~=0.29.0',
2828
'fire~=0.7.0',
2929
'ipython~=8.10.0',
3030
'jwcrypto~=1.5.6',
@@ -51,7 +51,7 @@ dependencies = [
5151
'uwsgi-readiness-check~=0.2.0',
5252
'django-allow-cidr',
5353
'django-csp~=3.7',
54-
'django-ansible-base[jwt-consumer,resource-registry]>=2025.8.18',
54+
"django-ansible-base[api-documentation,jwt-consumer,resource-registry]>=2026.1.26",
5555
"filelock==3.20.3",
5656
"pyasn1==0.6.2",
5757
]

requirements.txt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ django==4.2.27
127127
# social-auth-app-django
128128
django-allow-cidr==0.8.0
129129
# via ansible-ai-connect
130-
django-ansible-base==2025.12.3
130+
django-ansible-base==2026.1.26
131131
# via ansible-ai-connect
132132
django-crum==0.7.9
133133
# via django-ansible-base
@@ -152,8 +152,10 @@ djangorestframework==3.15.2
152152
# ansible-ai-connect
153153
# django-ansible-base
154154
# drf-spectacular
155-
drf-spectacular==0.27.2
156-
# via ansible-ai-connect
155+
drf-spectacular==0.29.0
156+
# via
157+
# ansible-ai-connect
158+
# django-ansible-base
157159
dynaconf==3.2.12
158160
# via django-ansible-base
159161
et-xmlfile==2.0.0
@@ -428,6 +430,7 @@ pyyaml==6.0.3
428430
referencing==0.36.2
429431
# via
430432
# ansible-lint
433+
# django-ansible-base
431434
# jsonschema
432435
# jsonschema-specifications
433436
requests==2.32.5

0 commit comments

Comments
 (0)