From 64378ba0b806135a76b8a1d8bf8a88ee75e7ead5 Mon Sep 17 00:00:00 2001 From: Mardone Date: Wed, 18 Dec 2024 17:08:07 -0300 Subject: [PATCH 1/2] remove zeroshot base --- nexus/settings.py | 3 ++ nexus/zeroshot/api/views.py | 6 ++- nexus/zeroshot/client.py | 42 +++++++++++++++++++ .../migrations/0002_zeroshotlogs_model.py | 18 ++++++++ nexus/zeroshot/models.py | 1 + router/entities/flow.py | 8 ++-- 6 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 nexus/zeroshot/migrations/0002_zeroshotlogs_model.py diff --git a/nexus/settings.py b/nexus/settings.py index 7ddfa8f7..421314f4 100644 --- a/nexus/settings.py +++ b/nexus/settings.py @@ -500,6 +500,9 @@ FUNCTION_CALLING_CHATGPT_MODEL = env.str("FUNCTION_CALLING_CHATGPT_MODEL", "gpt-4o-mini") FUNCTION_CALLING_CHATGPT_PROMPT = env.str("FUNCTION_CALLING_CHATGPT_PROMPT", "") +# Classification data +DEFAULT_CLASSIFICATION_MODEL = env.str("DEFAULT_CLASSIFICATION_MODEL", ZEROSHOT_MODEL_BACKEND) + # Reflection data GROUNDEDNESS_MODEL = env.str("GROUNDEDNESS_MODEL", "gpt-4o-mini") GROUNDEDNESS_SYSTEM_PROMPT = env.str("GROUNDEDNESS_SYSTEM_PROMPT", "") diff --git a/nexus/zeroshot/api/views.py b/nexus/zeroshot/api/views.py index 8442397d..1c516fde 100644 --- a/nexus/zeroshot/api/views.py +++ b/nexus/zeroshot/api/views.py @@ -9,6 +9,8 @@ from nexus.zeroshot.api.permissions import ZeroshotTokenPermission from nexus.zeroshot.models import ZeroshotLogs +from django.conf import settings + logger = logging.getLogger(__name__) @@ -21,6 +23,7 @@ class ZeroShotFastPredictAPIView(APIView): def post(self, request): data = request.data try: + invoke_model = InvokeModel(data) response = invoke_model.invoke() @@ -30,7 +33,8 @@ def post(self, request): other=response["output"].get("other", False), options=data.get("options"), nlp_log=str(json.dumps(response)), - language=data.get("language") + language=data.get("language"), + model=settings.DEFAULT_CLASSIFICATION_MODEL ) return Response(status=200, data=response if response.get("output") else {"error": response}) diff --git a/nexus/zeroshot/client.py b/nexus/zeroshot/client.py index c5a3c607..3c1c83e1 100644 --- a/nexus/zeroshot/client.py +++ b/nexus/zeroshot/client.py @@ -7,6 +7,9 @@ from nexus.zeroshot.format_classification import FormatClassification from nexus.zeroshot.format_prompt import FormatPrompt +from router.classifiers.chatgpt_function import ChatGPTFunctionClassifier +from router.entities.flow import FlowDTO + class InvokeModel: def __init__( @@ -112,8 +115,47 @@ def _invoke_zeroshot(self, model_backend: str): "bedrock": self._invoke_bedrock }.get(model_backend) + def _invoke_function_calling(self): + # request_data = { + # "context": "Example context", + # "language": "por", + # "text": "This is a sample text", + # "options": [ + # { + # "class": "option-class-1", + # "context": "Option 1 context" + # }, + # { + # "class": "option-class-2", + # "context": "Option 2 context" + # }, + # { + # "class": "option-class-3", + # "context": "Option 3 context" + # } + # ] + # } + + + classifier = ChatGPTFunctionClassifier( + agent_goal=self.zeroshot_data.get("context"), + ) + + flow_dto_list = [] + for option in self.zeroshot_data.get("options"): + flow_dto_list.append(FlowDTO(name=option.get("class"), prompt=option.get("context"))) + + prediction = classifier.predict( + message=self.zeroshot_data.get("text"), + flows=flow_dto_list, + language=self.zeroshot_data.get("language") + ) + + def invoke(self): prompt = self._get_prompt(self.zeroshot_data) + if settings.DEFAULT_CLASSIFICATION_MODEL != "zeroshot": + return self._invoke_function_calling() invoke_zeroshot = self._invoke_zeroshot(self.model_backend) if invoke_zeroshot: return invoke_zeroshot(prompt) diff --git a/nexus/zeroshot/migrations/0002_zeroshotlogs_model.py b/nexus/zeroshot/migrations/0002_zeroshotlogs_model.py new file mode 100644 index 00000000..eacc7a32 --- /dev/null +++ b/nexus/zeroshot/migrations/0002_zeroshotlogs_model.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.10 on 2024-12-18 18:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('zeroshot', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='zeroshotlogs', + name='model', + field=models.CharField(default='zeroshot', max_length=64, verbose_name='Model'), + ), + ] diff --git a/nexus/zeroshot/models.py b/nexus/zeroshot/models.py index 7f8f38b3..cc085eec 100644 --- a/nexus/zeroshot/models.py +++ b/nexus/zeroshot/models.py @@ -18,3 +18,4 @@ class Meta: nlp_log = models.TextField(blank=True) created_at = models.DateTimeField("created at", auto_now_add=True) language = models.CharField(verbose_name="Language", max_length=64, null=True, blank=True) + model = models.CharField(verbose_name="Model", max_length=64, default="zeroshot") diff --git a/router/entities/flow.py b/router/entities/flow.py index 180f5c98..24b89f12 100644 --- a/router/entities/flow.py +++ b/router/entities/flow.py @@ -3,10 +3,10 @@ @dataclass class FlowDTO: - pk: str - uuid: str name: str prompt: str - fallback: str - content_base_uuid: str + pk: str = None + uuid: str = None + fallback: str = None + content_base_uuid: str = None send_to_llm: bool = False From 09bbe31315728608cb02085996a2152f775f5a2d Mon Sep 17 00:00:00 2001 From: Mardone Date: Fri, 20 Dec 2024 10:39:42 -0300 Subject: [PATCH 2/2] Add unittest and format response --- nexus/zeroshot/client.py | 34 ++++++---------- nexus/zeroshot/format_classification.py | 16 ++++++++ nexus/zeroshot/tests.py | 53 ++++++++++++++++++++++++- router/classifiers/chatgpt_function.py | 7 +++- 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/nexus/zeroshot/client.py b/nexus/zeroshot/client.py index 3c1c83e1..e3cf26a9 100644 --- a/nexus/zeroshot/client.py +++ b/nexus/zeroshot/client.py @@ -116,41 +116,31 @@ def _invoke_zeroshot(self, model_backend: str): }.get(model_backend) def _invoke_function_calling(self): - # request_data = { - # "context": "Example context", - # "language": "por", - # "text": "This is a sample text", - # "options": [ - # { - # "class": "option-class-1", - # "context": "Option 1 context" - # }, - # { - # "class": "option-class-2", - # "context": "Option 2 context" - # }, - # { - # "class": "option-class-3", - # "context": "Option 3 context" - # } - # ] - # } - classifier = ChatGPTFunctionClassifier( agent_goal=self.zeroshot_data.get("context"), ) flow_dto_list = [] - for option in self.zeroshot_data.get("options"): + options = self.zeroshot_data.get("options", []) + for option in options: flow_dto_list.append(FlowDTO(name=option.get("class"), prompt=option.get("context"))) - prediction = classifier.predict( + prediction: str = classifier.predict( message=self.zeroshot_data.get("text"), flows=flow_dto_list, language=self.zeroshot_data.get("language") ) + formated_prediction = { + "output": prediction + } + + classification_formater = FormatClassification(formated_prediction) + formatted_classification = classification_formater.get_classification(self.zeroshot_data) + + response = {"output": formatted_classification} + return response def invoke(self): prompt = self._get_prompt(self.zeroshot_data) diff --git a/nexus/zeroshot/format_classification.py b/nexus/zeroshot/format_classification.py index 9ec5e9e7..66c82fb2 100644 --- a/nexus/zeroshot/format_classification.py +++ b/nexus/zeroshot/format_classification.py @@ -11,6 +11,9 @@ def __init__(self, classification_data: dict): self.classification_data = classification_data def get_classification(self, zeroshot_data): + if settings.DEFAULT_CLASSIFICATION_MODEL != "zeroshot": + return self._get_function_calling_classification(zeroshot_data) + if self.model_backend == "runpod": return self._get_runpod_classification(zeroshot_data) elif self.model_backend == "bedrock": @@ -55,3 +58,16 @@ def _get_runpod_classification(self, zeroshot_data): def _get_bedrock_classification(self, zeroshot_data): output_text = self.classification_data.get("outputs")[0].get("text").strip() return self._get_formatted_output(output_text, zeroshot_data) + + def _get_function_calling_classification(self, zeroshot_data): + output_text = self.classification_data.get("output") + classification = {"other": True, "classification": self._get_data_none_class()} + + if output_text: + response_prepared = output_text.strip().strip(".").strip("\n").strip("'").lower() + all_classes = [option.get("class").lower() for option in zeroshot_data.get("options", [])] + + if response_prepared in all_classes: + classification["other"] = False + classification["classification"] = response_prepared + return classification diff --git a/nexus/zeroshot/tests.py b/nexus/zeroshot/tests.py index a61dc99b..34772949 100644 --- a/nexus/zeroshot/tests.py +++ b/nexus/zeroshot/tests.py @@ -1,6 +1,8 @@ -from django.test import TestCase +from django.test import TestCase, override_settings from nexus.zeroshot.client import InvokeModel -from unittest.mock import patch +from unittest.mock import patch, ANY + +from router.classifiers.chatgpt_function import ChatGPTFunctionClassifier class TestClient(TestCase): @@ -12,19 +14,66 @@ def setUp(self) -> None: 'options': [] } + @override_settings(DEFAULT_CLASSIFICATION_MODEL="zeroshot") @patch("nexus.zeroshot.client.InvokeModel._invoke_bedrock") def test_call_bedrock(self, mock): invoke_model = InvokeModel(self.zeroshot_data, model_backend="bedrock") invoke_model.invoke() self.assertTrue(mock.called) + @override_settings(DEFAULT_CLASSIFICATION_MODEL="zeroshot") @patch("nexus.zeroshot.client.InvokeModel._invoke_runpod") def test_call_runpod(self, mock): invoke_model = InvokeModel(self.zeroshot_data, model_backend="runpod") invoke_model.invoke() self.assertTrue(mock.called) + @override_settings(DEFAULT_CLASSIFICATION_MODEL="zeroshot") def test_value_error(self): invoke_model = InvokeModel(self.zeroshot_data, model_backend="err") with self.assertRaises(ValueError): invoke_model.invoke() + + @override_settings(DEFAULT_CLASSIFICATION_MODEL="function_calling") + @patch("nexus.zeroshot.client.InvokeModel._invoke_function_calling") + def test_call_function_calling(self, mock): + invoke_model = InvokeModel(self.zeroshot_data, model_backend="zeroshot") + invoke_model.invoke() + self.assertTrue(mock.called) + + +class TestFunctionCalling(TestCase): + + @override_settings(DEFAULT_CLASSIFICATION_MODEL='function_calling') + def test_invoke_function_calling_calls_correct_methods(self): + zeroshot_data = { + "context": "This is the agent goal.", + "language": "eng", + "text": "User message to classify.", + "options": [ + {"class": "Class1", "context": "Context for class 1"}, + {"class": "Class2", "context": "Context for class 2"}, + ] + } + + with patch.object(ChatGPTFunctionClassifier, '__init__', return_value=None) as mock_init, \ + patch.object(ChatGPTFunctionClassifier, 'predict', return_value='Class1') as mock_predict: + + invoke_model = InvokeModel(zeroshot_data) + response = invoke_model.invoke() + + mock_init.assert_called_with(agent_goal='This is the agent goal.') + + mock_predict.assert_called_with( + message='User message to classify.', + flows=ANY, + language='eng', + ) + + expected_response = { + 'output': { + 'other': False, + 'classification': 'class1' + } + } + self.assertEqual(response, expected_response) diff --git a/router/classifiers/chatgpt_function.py b/router/classifiers/chatgpt_function.py index 94fc121f..af316f7a 100644 --- a/router/classifiers/chatgpt_function.py +++ b/router/classifiers/chatgpt_function.py @@ -88,11 +88,16 @@ def predict( self, message: str, flows: List[FlowDTO], + custom_prompt: str = None, language: str = "por" ) -> str: print(f"[+ ChatGPT message function classification: {message} ({language}) +]") - formated_prompt = self.get_prompt() + + formated_prompt = custom_prompt + if not custom_prompt: + formated_prompt = self.get_prompt() + msg = [ { "role": "system",