From 6d27b40e99328b684f17d6def8f2adb56bc2fab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A9ri=20Le=20Bouder?= Date: Mon, 13 Jan 2025 14:25:13 -0500 Subject: [PATCH] AAP-36665: Telemetry events for Roles Also, move the view to `AACSAPIView`. --- ansible_ai_connect/ai/api/serializers.py | 27 +---- .../ai/api/telemetry/schema1.py | 10 ++ ansible_ai_connect/ai/api/tests/test_views.py | 9 +- ansible_ai_connect/ai/api/views.py | 102 +++++++----------- .../ansible-ai-connect-service.yaml | 6 ++ 5 files changed, 63 insertions(+), 91 deletions(-) diff --git a/ansible_ai_connect/ai/api/serializers.py b/ansible_ai_connect/ai/api/serializers.py index 9a4f21072..d824064a7 100644 --- a/ansible_ai_connect/ai/api/serializers.py +++ b/ansible_ai_connect/ai/api/serializers.py @@ -464,17 +464,6 @@ class ExplanationResponseSerializer(serializers.Serializer): class GenerationPlaybookRequestSerializer(serializers.Serializer): - class Meta: - fields = [ - "text", - "generationId", - "wizardId", - "createOutline", - "customPrompt", - "ansibleExtensionVersion", - "outline", - ] - text = AnonymizedCharField( required=True, label="Description content", @@ -539,18 +528,6 @@ def validate(self, data): class GenerationRoleRequestSerializer(serializers.Serializer): - class Meta: - fields = [ - "text", - "outline", - "createOutline", - "additionalContext", - "fileTypes", - "generationId", - "wizardId", - "ansibleExtensionVersion", - ] - text = AnonymizedCharField( required=True, label="the goal of the role", @@ -563,6 +540,7 @@ class Meta: required=False, label="an outline of the role", help_text="An outline of the role should be a numbered list.", + default="", ) createOutline = serializers.BooleanField( required=False, @@ -588,18 +566,21 @@ class Meta: "The file type name is based on the inner role directories, " "without the trailing 's'" ), + default=["task", "default"], ) generationId = serializers.UUIDField( format="hex_verbose", required=False, label="generation ID", help_text=("A UUID that identifies the particular generation data is being requested for."), + default="", ) wizardId = serializers.UUIDField( format="hex_verbose", required=False, label="wizard ID", help_text=("A UUID to track the succession of interaction from the user."), + default="", ) model = serializers.CharField(required=False, allow_blank=True) diff --git a/ansible_ai_connect/ai/api/telemetry/schema1.py b/ansible_ai_connect/ai/api/telemetry/schema1.py index e1b1673fd..33b3e6db9 100644 --- a/ansible_ai_connect/ai/api/telemetry/schema1.py +++ b/ansible_ai_connect/ai/api/telemetry/schema1.py @@ -173,6 +173,16 @@ class GenerationPlaybookEvent(Schema1Event): ) +@define +class GenerationRoleEvent(Schema1Event): + event_name: str = "codegenRole" + generationId: str = field(validator=validators.instance_of(str), converter=str, default="") + wizardId: str = field(validator=validators.instance_of(str), converter=str, default="") + create_outline: bool = field( + validator=validators.instance_of(bool), converter=bool, default=False + ) + + @define class ChatBotResponseDocsReferences: docs_url: str = field(validator=validators.instance_of(str), converter=str, default="") diff --git a/ansible_ai_connect/ai/api/tests/test_views.py b/ansible_ai_connect/ai/api/tests/test_views.py index 0e6379db3..8af1808a2 100644 --- a/ansible_ai_connect/ai/api/tests/test_views.py +++ b/ansible_ai_connect/ai/api/tests/test_views.py @@ -3509,14 +3509,17 @@ def test_with_custom_prompt_missing_outline_when_not_needed(self): @override_settings(ANSIBLE_AI_MODEL_MESH_CONFIG=mock_config("dummy")) class TestRoleGenerationView(WisdomAppsBackendMocking, WisdomServiceAPITestCaseBase): def test_ok(self): - generation_id = str(uuid.uuid4()) + generation_id = uuid.uuid4() payload = { "text": "Install nginx and enable the service", "generationId": generation_id, "ansibleExtensionVersion": "24.4.0", } self.client.force_authenticate(user=self.user) - r = self.client.post(reverse("generations/role"), payload, format="json") + with self.assertLogs(logger="root", level="DEBUG") as log: + r = self.client.post(reverse("generations/role"), payload, format="json") + segment_events = self.extractSegmentEventsFromLog(log) + roleGenEvent = segment_events[0] self.assertEqual(r.status_code, HTTPStatus.OK) self.assertIsNotNone(r.data) self.assertEqual(r.data["files"], ROLE_FILES) @@ -3524,6 +3527,8 @@ def test_ok(self): self.assertEqual(r.data["generationId"], generation_id) self.assertEqual(r.data["outline"], "") self.assertEqual(r.data["role"], "install_nginx") + self.assertEqual(roleGenEvent["event"], "codegenRole") + self.assertEqual(roleGenEvent["properties"]["generationId"], str(generation_id)) def test_unauthorized(self): payload = {} diff --git a/ansible_ai_connect/ai/api/views.py b/ansible_ai_connect/ai/api/views.py index ac6595fae..a2f61a327 100644 --- a/ansible_ai_connect/ai/api/views.py +++ b/ansible_ai_connect/ai/api/views.py @@ -192,9 +192,9 @@ def initialize_request(self, request, *args, **kwargs): return initialised_request def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) if hasattr(request, "data"): self.load_parameters(request) - return super().initial(request, *args, **kwargs) def load_parameters(self, request) -> Response: if hasattr(self, "request_serializer_class"): @@ -870,7 +870,7 @@ def post(self, request) -> Response: ) -class GenerationRole(APIView): +class GenerationRole(AACSAPIView): """ Returns a role based on a text input. """ @@ -883,7 +883,8 @@ class GenerationRole(APIView): IsAuthenticatedOrTokenHasScope, ] required_scopes = ["read", "write"] - + schema1_event = schema1.GenerationRoleEvent + request_serializer_class = GenerationRoleRequestSerializer throttle_cache_key_suffix = "_generation_role" @extend_schema( @@ -895,73 +896,42 @@ class GenerationRole(APIView): summary="Inline code suggestions", ) def post(self, request) -> Response: - # Declaring but commenting out variables to define them but also comply pre-commit - text = "" - outline = "" - create_outline = False - additional_context = {} - file_types = ["task", "default"] - generation_id = "" - wizard_id = "" - request_serializer = GenerationRoleRequestSerializer(data=request.data) - answer = {} - model_id = "" - try: - request_serializer.is_valid(raise_exception=True) - - text = request_serializer.validated_data["text"] - outline = str(request_serializer.validated_data.get("outline", outline)) - create_outline = request_serializer.validated_data["createOutline"] - additional_context = request_serializer.validated_data.get( - "additionalContext", additional_context - ) - file_types = request_serializer.validated_data.get("fileTypes", file_types) - generation_id = str( - request_serializer.validated_data.get("generationId", generation_id) - ) - wizard_id = str(request_serializer.validated_data.get("wizardId", wizard_id)) - model_id = str(request_serializer.validated_data.get("model", "")) - - llm: ModelPipelineRoleGeneration = apps.get_app_config("ai").get_model_pipeline( - ModelPipelineRoleGeneration - ) + self.event.create_outline = self.validated_data["createOutline"] + self.event.generationId = self.validated_data["generationId"] + self.event.wizardId = self.validated_data["wizardId"] + llm: ModelPipelineRoleGeneration = apps.get_app_config("ai").get_model_pipeline( + ModelPipelineRoleGeneration + ) - roles, files, outline = llm.invoke( - RoleGenerationParameters.init( - request=request, - text=text, - outline=outline, - model_id=model_id, - create_outline=create_outline, - additional_context=additional_context, - file_types=file_types, - generation_id=generation_id, - ) + roles, files, outline = llm.invoke( + RoleGenerationParameters.init( + request=request, + text=self.validated_data["text"], + outline=self.validated_data["outline"], + model_id=self.req_model_id, + create_outline=self.validated_data["createOutline"], + additional_context=self.validated_data.get("additionalContext", {}), + file_types=self.validated_data["fileTypes"], + generation_id=self.validated_data["generationId"], ) + ) - # Anonymize responses - # Anonymized in the View to be consistent with where Completions are anonymized - anonymized_role = anonymizer.anonymize_struct( - roles, value_template=Template("{{ _${variable_name}_ }}") - ) - anonymized_outline = anonymizer.anonymize_struct( - outline, value_template=Template("{{ _${variable_name}_ }}") - ) + # Anonymize responses + # Anonymized in the View to be consistent with where Completions are anonymized + anonymized_role = anonymizer.anonymize_struct( + roles, value_template=Template("{{ _${variable_name}_ }}") + ) + anonymized_outline = anonymizer.anonymize_struct( + outline, value_template=Template("{{ _${variable_name}_ }}") + ) - answer = { - "role": anonymized_role, - "outline": anonymized_outline, - "files": files, - "format": "plaintext", - "generationId": generation_id, - } - ################################################## - except Exception as e: - logger.exception(f"An exception {e.__class__} occurred during a role generation") - raise - finally: - # implement write to segment there. - pass + answer = { + "role": anonymized_role, + "outline": anonymized_outline, + "files": files, + "format": "plaintext", + "generationId": self.validated_data["generationId"], + } return Response( answer, diff --git a/tools/openapi-schema/ansible-ai-connect-service.yaml b/tools/openapi-schema/ansible-ai-connect-service.yaml index f78d46c4d..dd8fbf781 100644 --- a/tools/openapi-schema/ansible-ai-connect-service.yaml +++ b/tools/openapi-schema/ansible-ai-connect-service.yaml @@ -1056,6 +1056,7 @@ components: as appropriate, and reject input above a certain HAP threshold. outline: type: string + default: '' title: an outline of the role description: An outline of the role should be a numbered list. createOutline: @@ -1073,6 +1074,9 @@ components: type: array items: type: string + default: + - task + - default title: file types description: The file types generated by the model. Default is ['task', 'default']. The file type name is based on the inner role directories, @@ -1080,12 +1084,14 @@ components: generationId: type: string format: uuid + default: '' title: generation ID description: A UUID that identifies the particular generation data is being requested for. wizardId: type: string format: uuid + default: '' title: wizard ID description: A UUID to track the succession of interaction from the user. model: