From 89337418f6b6bd2f8edd455c0dea7b49b8ea9193 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Fri, 24 Oct 2025 19:03:53 +0200
Subject: [PATCH] ai models and calls: standardized and harmonized all model
operation types
---
env_dev.env | 19 +-
env_int.env | 19 +-
env_prod.env | 19 +-
modules/aicore/aicoreBase.py | 4 -
modules/aicore/aicoreModelRegistry.py | 4 -
modules/aicore/aicoreModelSelector.py | 40 +-
modules/aicore/aicorePluginAnthropic.py | 98 ++--
modules/aicore/aicorePluginInternal.py | 38 +-
modules/aicore/aicorePluginOpenai.py | 187 ++++---
modules/aicore/aicorePluginPerplexity.py | 334 ++++++++----
modules/aicore/aicorePluginTavily.py | 113 ++--
modules/datamodels/__init__.py | 3 +-
modules/datamodels/datamodelAi.py | 100 ++--
modules/interfaces/interfaceAiObjects.py | 37 +-
.../serviceAi/subDocumentGeneration.py | 4 +-
.../serviceAi/subDocumentProcessing.py | 10 +-
.../renderers/rendererBaseTemplate.py | 2 +-
.../renderers/rendererImage.py | 2 +-
.../renderers/rendererPdf.py | 2 +-
.../renderers/rendererPptx.py | 2 +-
.../renderers/rendererXlsx.py | 2 +-
.../serviceGeneration/subPromptBuilder.py | 2 +-
.../services/serviceUtils/mainServiceUtils.py | 25 +-
modules/workflows/methods/methodAi.py | 165 +-----
.../processing/adaptive/contentValidator.py | 2 +-
.../processing/adaptive/intentAnalyzer.py | 2 +-
.../processing/modes/modeActionplan.py | 2 +-
.../workflows/processing/modes/modeReact.py | 8 +-
test_ai_model_selection.py | 495 ++++++++++++------
test_operation_type_ratings.py | 97 ++++
30 files changed, 1121 insertions(+), 716 deletions(-)
create mode 100644 test_operation_type_ratings.py
diff --git a/env_dev.env b/env_dev.env
index 3fcef74e..e1dd2756 100644
--- a/env_dev.env
+++ b/env_dev.env
@@ -50,23 +50,11 @@ APP_LOGGING_BACKUP_COUNT = 5
Service_MSFT_REDIRECT_URI = http://localhost:8000/api/msft/auth/callback
Service_GOOGLE_REDIRECT_URI = http://localhost:8000/api/google/auth/callback
-# OpenAI configuration
-Connector_AiOpenai_API_URL = https://api.openai.com/v1/chat/completions
+# AI configuration
Connector_AiOpenai_API_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEajBuZmtYTVdqLTBpQm9KZ2pCXzRCV3VhZzlYTEhKb1FqWXNrV3lyb25uZUN1WVVQUEY3dGYtejludV9MNGlKeVREanZGOGloV09mY2ttQ3k5SjBFOGFac2ZQTkNKNUZWVnRINVQyeWhsR2wyYnVrRDNzV2NqSHB0ajQ4UWtGeGZtbmR0Q3VvS0hDZlphVmpSc2Z6RG5nPT0=
-Connector_AiOpenai_MODEL_NAME = gpt-4o
-Connector_AiOpenai_TEMPERATURE = 0.2
-
-# Anthropic configuration
-Connector_AiAnthropic_API_URL = https://api.anthropic.com/v1/messages
Connector_AiAnthropic_API_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpENmFBWG16STFQUVZxNzZZRzRLYTA4X3lRanF1VkF4cU45OExNMzlsQmdISGFxTUxud1dXODBKcFhMVG9KNjdWVnlTTFFROVc3NDlsdlNHLUJXeG41NDBHaXhHR0VHVWl5UW9RNkVWbmlhakRKVW5pM0R4VHk0LUw0TV9LdkljNHdBLXJua21NQkl2b3l4UkVkMGN1YjBrMmJEeWtMay1jbmxrYWJNbUV0aktCXzU1djR2d2RSQXZORTNwcG92ZUVvVGMtQzQzTTVncEZTRGRtZUFIZWQ0dz09
-Connector_AiAnthropic_MODEL_NAME = claude-3-5-sonnet-20241022
-Connector_AiAnthropic_TEMPERATURE = 0.2
-
-# Perplexity AI configuration
-Connector_AiPerplexity_API_URL = https://api.perplexity.ai/chat/completions
Connector_AiPerplexity_API_SECRET = DEV_ENC:Z0FBQUFBQm82Mzk2Q1MwZ0dNcUVBcUtuRDJIcTZkMXVvYnpjM3JEMzJiT1NKSHljX282ZDIyZTJYc09VSTdVNXAtOWU2UXp5S193NTk5dHJsWlFjRjhWektFOG1DVGY4ZUhHTXMzS0RPN1lNcF9nSlVWbW5BZ1hkZDVTejl6bVZNRFVvX29xamJidWRFMmtjQmkyRUQ2RUh6UTN1aWNPSUJBPT0=
-Connector_AiPerplexity_MODEL_NAME = sonar
-Connector_AiPerplexity_TEMPERATURE = 0.2
+Connector_AiTavily_API_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEQTdnUHMwd2pIaXNtMmtCTFREd0pyQXRKb1F5eGtHSnkyOGZiUnlBOFc0b3Vzcndrc3ViRm1nMDJIOEZKYWxqdWNkZGh5N0Z4R0JlQmxXSG5pVnJUR2VYckZhMWNMZ1FNeXJ3enJLVlpiblhOZTNleUg3ZzZyUzRZanFSeDlVMkI=
# Agent Mail configuration
Service_MSFT_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
@@ -77,9 +65,6 @@ Service_MSFT_TENANT_ID = common
Service_GOOGLE_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpETDJhbGVQMHlFQzNPVFI1ZzBMa3pNMGlQUHhaQm10eVl1bFlSeTBybzlTOWE2MURXQ0hkRlo0NlNGbHQxWEl1OVkxQnVKYlhhOXR1cUF4T3k0WDdscktkY1oyYllRTmdDTWpfbUdwWGtSd1JvNlYxeTBJdEtaaS1vYnItcW0yaFM=
-# Tavily Web Search configuration
-Connector_WebTavily_API_KEY_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpEQTdnUHMwd2pIaXNtMmtCTFREd0pyQXRKb1F5eGtHSnkyOGZiUnlBOFc0b3Vzcndrc3ViRm1nMDJIOEZKYWxqdWNkZGh5N0Z4R0JlQmxXSG5pVnJUR2VYckZhMWNMZ1FNeXJ3enJLVlpiblhOZTNleUg3ZzZyUzRZanFSeDlVMkI=
-
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpETk5FWWM3Q0JKMzhIYTlyMkhuNjA4NlF4dk82U2NScHhTVGY3UG83NkhfX3RrcWVtWWcyLXRjU1dTT21zWEl6YWRMMUFndXpsUnJOeHh3QThsNDZKRXROTzdXRUdsT0JZajZJNVlfb0gtMXkwWm9DOERPVnpjU0pyUEZfOGJsUnprT3ltMVVhalUyUm9hMUFtZEtHUnJqOGZ4dEZjZm5SWVVTckVCWnY1UkdVSHVmUlgwbnAyc0xDQW84R3ViSko5OHVCVWZRUVNiaG1pVFB6X3EwS0FPd2dUYjhiSmRjcXh2WEZiXzI4SFZqT21tbDduUWRyVWdFZXpmcVM5ZDR0VWtzZnF5UER6cGwwS2JlLV9CSTZ0Z0IyQ1h0YW9TcmhRTXZEckp4bWhmTkt6UTNYMk4zVkpnbUJmaDIxZnoyR2dWTEYwTUFEV0w2eUdUUGpoZk9XRkt4RVF1Z1NPdUpBeTcyWV9PY1Ffd2s0ZEdVekxGekhoeEl4TmNqaXYtbUJuSVdycFducERWdWtZajZnX011Q2w4eE9VMTBqQ1ZxRmdScWhXY1E3WWhzX1JZcHhxam9FbDVPN3Q1MWtrMUZuTUg3LVFQVHp1T1hpQWNDMzEzekVJWk9ybl91YUVjSkFob1VaMi1ONEtuMnRSOEg1S3QybUMwbVZDejItajBLTjM2Zy1hNzZQMW5LLVVDVGdFWm5BZUxNeEFnUkZzU3dxV0lCUlc0LWo4b05GczVpOGZSV2ZxbFBwUml6OU5tYjdnTks3Y3hrVEZVTHlmc1NPdFh4WE5pWldEZklOQUxBbjBpMTlkX3FFQVJ6c2NSZGdzTThycE92VW82enZKamhiRGFnU25aZGlHZHhZd2lUUmhuTVptNjhoWVlJQkxIOEkzbzJNMjZCZFJyM25tdXBnQ2ZWaHV3b2p6UWJpdk9xUEhBc1dyTlNmeF9wbm5yYUhHV01UZnVXWDFlNzBkdXlWUWhvcmJpSmljbmE3LUpUZEg4VzRwZ2JVSjdYUm1sODViQXVxUzdGTmZFbVpiN2V1YW5XV3U4b2VRWmxldGVGVHZsSldoekhVLU9wZ2V0cGZIYkNqM2pXVGctQVAyUm4xTHhpd1VVLXFhcnVEV21Rby1hbTlqTl84TjVveHdYTExUVkhHQ0ltaTB2WXJnY1NQVE5PbWg3ejgySElYc1JSTlQ3NDlFUWR6STZVUjVqaXFRN200NF9LY1ljQ0R2UldlWUtKY1NQVnJ4QXRyYTBGSWVuenhyM0Z0cWtndTd1eG8xRzY5a2dNZ1hkQm5MV3BHVzA2N1QwUkd6WlRGYTZQOUhnVWQ2S0Y5U0s1dXFNVXh5Q2pLWVUxSUQ2MlR1ak52NmRIZ2hlYTk1SGZGWS1RV3hWVU9rR3d1Rk9MLS11REZXbzhqMHpsSm1HYW1jMUNLT29YOHZsRWNaLTVvOFpmT3l3MHVwaERTT0dNLWFjcGRYZ25qT2szTkVFUnRFR3JWYS1aNXFIRnMyalozTlQzNFF2NXJLVHVPVF9zdTF6ZjlkbzJ4RFc2ZENmNFFxZDZzTzhfMUl0bW96V0lPZkh1dXFYZlEteFBlSG84Si1FNS1TTi1OMkFnX2pOYW8xY3MxMVJnVC02MDUyaXZfMEVHWDQtVlRpcENmV0h3V0dCWEFRS2prQXdNRlQ5dnRFVHU0Q1dNTmh0SlBCaU55bFMydWM1TTFFLW96ODBnV3dNZHFZTWZhRURYSHlrdzF3RlRuWDBoQUhSOUJWemtRM3pxcDJFbGJoaTJ3ZktRTlJxbXltaHBoZXVJVDlxS3cxNWo2c0ZBV0NzaUstRWdsMW1xLXFkanZGYUFiU0tSLXFQa0tkcDFoMV9kak41ZjQ0R214UmtOR1ZBanRuemY3Mmw1SkZ5aDZodGIzT3N2aV85MW9kcld6c0g0ZDgtTWo3b3Y3VjJCRnR2U2tMVm9rUXNVRnVHbzZXVTZ6RmI2RkNmajBfMWVnODVFbnpkT0oyci15czJHU0p1cUowTGZJMzVnd3hIRjQyTVhKOGRkcFRKdVpyQ3Yzd01Jb1lSajFmV0paeEV0cjk1SmpmdWpDVFJMUmMtUFctOGhaTmlKQXNRVlVUNlhJemxudHZCR056SVlBb3NOTEYxRTRLaFlVd2d3TWtxVlB6ZEtQLTkxOGMyY3N0a2pYRFUweDBNaGhja2xSSklPOUZla1dKTWRNbG8tUGdSNEV5cW90OWlOZFlIUExBd3U2b2hyS1owbXVMM3p0Qm41cUtzWUxYNzB1N3JpUTNBSGdsT0NuamNTb1lIbXR4MG1sakNPVkxBUXRLVE1xX0YxWDhOcERIY1lTQVFqS01CaXZKNllFaXlIR0JsM1pKMmV1OUo3TGI1WkRaVnYxUTl1LTM0SU1qN1V1b0RCT0x0VHNLTmNLZnk1S0MxYnBBcm03WnVua0xqaEhGUzhOU253ZkppRzdudXBSVlMxeFVOSWxtZ1o2RVBSQUhEUEFuQ1hxSVZMME4yWUtaU3VyRGo3RkUyRUNjT0pNcE1BdE1ZRzdXVl8ydUtXZjdMdHdEVW4teHUtTi1HSGliLUxud21TX0NtcGVkRFBHNkZ1WTlNczR4OUJfUVluc1BoV09oWS1scUdsNnB5d1U5M1huX3k4QzAyNldtb2hybktYN2xKZ1NTNWFsaWwzV3pCRVhkaGR5eTNlV1d6ZzFfaFZTT0E4UjRpQ3pKdEZxUlJ6UFZXM3laUndyWEk2NlBXLUpoajVhZzVwQXpWVzUtVjVNZFBwdWdQa3AxZC1KdGdqNnhibjN4dmFYb2cxcEVwc1g5R09zRUdINUZtOE5QRjVUU0dpZy1QVl9odnFtVDNuWFZLSURtMXlSMlhRNTBWSVFJbEdOOWpfVWV0SmdRWDdlUXZZWE8xRUxDN1I0aEN6MHYwNzM1cmpJS0ZpMnBYWkxfb3FsbEV1VnlqWGxqdVJ6SHlwSjAzRlMycTBaQ295NXNnZERpUnJQcjhrUUd3bkI4bDVzRmxQblhkaFJPTTdISnVUQmhET3BOMTM4bjVvUEc2VmZhb2lrR1FyTUl2RWNEeGg0U0dsNnV6eU5zOUxiNDY5SXBxR0hBS00wOTgyWTFnWkQyaEtLVUloT3ZxZGh0RWVGRmJzenFsaUtfZENQM0JzdkVVeTdXR3hUSmJST1NBMUI1NkVFWncwNW5JZVVLX1p1RXdqVnFfQWpvQ08yQjZhN1NkTkpTSnUxOVRXZXE0WFEtZWxhZW1NNXYtQ2sya0VGLURmS01lMkctNVY3c2ZhN0ZGRFgwWHlabTFkeS1hcUZ1dDZ3cnpPQ3hha2IzVE11M0pqbklmU0diczBqTFBNZC1QZGp6VzNTSnJVSjJoWkJUQjVORG4tYUJmMEJtSUNUdVpEaGt6OTM3TjFOdVhXUHItZjRtZ25nU3NhZC1sVTVXNTRDTmxZbnlfeHNsdkpuMXhUYnE1MnpVQ0ZOclRWM1M4eHdXTzRXbFRZZVQtTS1iRVdXVWZMSGotcWg3MUxUYTFnSEEtanBCRHlZRUNIdGdpUFhsYjdYUndCZnRITzhMZVJ1dHFoVlVNb0duVjlxd0U4OGRuQVV3MG90R0hiYW5MWkxWVklzbWFRNzBfSUNrdzc5bVdtTXg0dExEYnRCaDI3c1I4TWFwLXZKR0wxSjRZYjZIV3ZqZjNqTWhFT0RGSDVMc1A1UzY2bDBiMGFSUy1fNVRQRzRJWDVydUpqb1ZfSHNVbldVeUN2YlAxSW5WVDdxVzJ1WHpLeUdmb0xWMDNHN05oQzY3YnhvUUdhS2xaOHNidkVvbTZtSHFlblhOYmwyR3NQdVJDRUdxREhWdF9ZcXhwUWxHc2hyLW5vUGhIUVhJNUNhY0hFU0ptVnI0TFVhZDE1TFBBUEstSkRoZWJ5MHJhUmZrR1ZrRlFtRGpxS1pOMmFMQjBsdjluY3FiYUU4eGJVVXlZVEpuNWdHVVhJMGtwaTdZR2NDbXd2eHpOQ09SeTV6N1BaVUpsR1pQVDBZcElJUUt6VnVpQmxSYnE4Y1BCWV9IRWdVV0p3enBGVHItdnBGN3NyNWFBWmkySnByWThsbDliSlExQmp3LVlBaDIyZXp6UnR6cU9rTzJmTDBlSVpON0tiWllMdm1oME1zTFl2S2ZYYllhQlY2VHNZRGtHUDY4U1lIVExLZTU4VzZxSTZrZHl1ZTBDc0g4SjI4WGYyZHV1bm9wQ3R2Z09ld1ZmUkN5alJGeHZKSHl1bWhQVXpNMzdjblpLcUhfSm02Qlh5S1FVN3lIcHl0NnlRPT0=
diff --git a/env_int.env b/env_int.env
index f7c35746..27210a43 100644
--- a/env_int.env
+++ b/env_int.env
@@ -50,23 +50,11 @@ APP_LOGGING_BACKUP_COUNT = 5
Service_MSFT_REDIRECT_URI = https://gateway-int.poweron-center.net/api/msft/auth/callback
Service_GOOGLE_REDIRECT_URI = https://gateway-int.poweron-center.net/api/google/auth/callback
-# OpenAI configuration
-Connector_AiOpenai_API_URL = https://api.openai.com/v1/chat/completions
+# AI configuration
Connector_AiOpenai_API_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjSDBNYkptSkQxTUotYVVpZVNZc0dxNGNwSEtkOEE0T3RZWjROTEhSRlRXdlZmQUxxZ0w3Y0xOV2JNV19LNF9yTUZiU1pUNG15U2VDUDdSVlI4VlpnR3JXVFFtcXBaTEZiaUtSclVFd0lCZG1rWVhra1dfWTVQOTBEYUU0MjByYVNEMTFmeXNOcmpUT216MmJKdlVPeW5nPT0=
-Connector_AiOpenai_MODEL_NAME = gpt-4o
-Connector_AiOpenai_TEMPERATURE = 0.2
-
-# Anthropic configuration
-Connector_AiAnthropic_API_URL = https://api.anthropic.com/v1/messages
Connector_AiAnthropic_API_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjT1ZlRWVJdVZMT3ljSFJDcFdxRFBRVkZhS204NnN5RDBlQ0tpenhTM0FFVktuWW9mWHNwRWx2dHB0eDBSZ0JFQnZKWlp6c01pVGREWHd1eGpERnU0Q2xhaks1clQ1ZXVsdnd2ZzhpNXNQS1BhY3FjSkdkVEhHalNaRGR4emhpakZncnpDQUVxOHVXQzVUWmtQc0FsYmFwTF9TSG5FOUFtWk5Ick1NcHFvY2s1T1c2WXlRUFFJZnh6TWhuaVpMYmppcDR0QUx0a0R6RXlwbGRYb1R4dzJkUT09
-Connector_AiAnthropic_MODEL_NAME = claude-3-5-sonnet-20241022
-Connector_AiAnthropic_TEMPERATURE = 0.2
-
-# Perplexity AI configuration
-Connector_AiPerplexity_API_URL = https://api.perplexity.ai/chat/completions
Connector_AiPerplexity_API_SECRET = INT_ENC:Z0FBQUFBQm82Mzk2UWZJdUFhSW8yc3RKc0tKRXphd0xWMkZOVlFpSGZ4SGhFWnk0cTF5VjlKQVZjdS1QSWdkS0pUSWw4OFU5MjUxdTVQel9aeWVIZTZ5TXRuVmFkZG0zWEdTOGdHMHpsTzI0TGlWYURKU1Q0VVpKTlhxUk5FTmN6SUJScDZ3ZldIaUJZcWpaQVRiSEpyQm9tRTNDWk9KTnZBPT0=
-Connector_AiPerplexity_MODEL_NAME = sonar
-Connector_AiPerplexity_TEMPERATURE = 0.2
+Connector_AiTavily_API_SECRET = INT_ENC:Z0FBQUFBQm8xSVRkdkJMTDY0akhXNzZDWHVYSEt1cDZoOWEzSktneHZEV2JndTNmWlNSMV9KbFNIZmQzeVlrNE5qUEIwcUlBSGM1a0hOZ3J6djIyOVhnZzI3M1dIUkdicl9FVXF3RGktMmlEYmhnaHJfWTdGUkktSXVUSGdQMC1vSEV6VE8zR2F1SVk=
# Agent Mail configuration
Service_MSFT_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
@@ -77,9 +65,6 @@ Service_MSFT_TENANT_ID = common
Service_GOOGLE_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNThGeVRNd3hacThtRnE0bzlDa0JPUWQyaEd6QjlFckdsMGZjRlRfUks2bXV3aDdVRTF3LVRlZVY5WjVzSXV4ZGNnX002RDl3dkNYdGFzZkxVUW01My1wTHRCanVCLUozZEx4TlduQlB5MnpvNTR2SGlvbFl1YkhzTEtsSi1SOEo=
-# Tavily Web Search configuration
-Connector_WebTavily_API_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRkdkJMTDY0akhXNzZDWHVYSEt1cDZoOWEzSktneHZEV2JndTNmWlNSMV9KbFNIZmQzeVlrNE5qUEIwcUlBSGM1a0hOZ3J6djIyOVhnZzI3M1dIUkdicl9FVXF3RGktMmlEYmhnaHJfWTdGUkktSXVUSGdQMC1vSEV6VE8zR2F1SVk=
-
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRkNmVXZ1pWcHcydTF2MXF0ZGJoWHBydF85bTczTktiaEJ3Wk1vMW1mZVhDSG1yd0ZxR2ZuSGJTX0N3MWptWXFJTkNTWjh1SUVVTXI4UDVzcGdLMkU5SHJ2TUpkRlRoRWdnSldtYjNTQkh4UDJHY2xmdTdZQ1ZiMTZZcGZxS3RzaHdjV3dtVkZUcEpJcWx0b2xuQVR6ZmpoVFZPY1hNMTV2SnhDaC1IZEh4UUpLTy1ILXA4RG1zamJTbUJ4X0t2M2NkdzJPbEJxSmFpRzV3WC0wZThoVzlxcmpHZ3ZkLVlVY3REZk1vV19WQ05BOWN6cnJ4MWNYYnNiQ0FQSUVnUlpfM3BhMnlsVlZUOG5wM3pzM1lSN1UzWlZKUXRLczlHbjI1LTFvSUJ4SlVXMy1BNk43bE5Hb0RfTTVlWk9oZnFIaVg0SW5pbm9EcXRTTzU1RFlYY3dTcnpKWWNyNjN5T1BGZ0FmX253cEFncmhvZVRuM05KYzhkOEhFMFJsc2NBSEwzZVZ1R0JMOGxsekVwUE55alZaRXFrdzNWWVNGWXNmbnhKeWhQSFo2VXBTUlRPeHdvdVdncEFuOWgydEtsSUFneUN6cGVaTnBSdjNCdVJseGJFdmlMc203UFhLVlYyTENkaGg2dVN6Z2xwT1ZmTmN5bVZGUkM3ZWcyVkt2ckFUVVd3WFFwYnJjNVRobEh2SkVJbXRwUUpEOFJKQ1NUc0Q4NHNqUFhPSDh5cTV6MEcwSDEwRUJCQ2JiTTJlOE5nd3pMMkJaQ1dVYjMwZVVWWnlETmp2dkZ3aXEtQ29WNkxZTFkzYUkxdTlQUU1OTnhWWU12YU9MVnJQa1d2ZjRtUlhneTNubEMxTmp1eUNPOThSMlB3Y1F0T2tCdFNsNFlKalZPV25yR2QycVBUb096RmZ1V0FTaGsxLV9FWDBmenBIOXpMdGpLcUc0TWRoY2hlMFhYTzlET1ZRekw0ZHNwUVBQdVJBX2h6Q2ZzWVZJWTNybTJiekp3WmhmWF9SUFBXQzlqUjctcVlHWWVMZWVQallzR0JGTVF0WmtnWlg1aTM1bFprNVExZXY5dnNvWF93UjhwbkJ3RzNXaVJ2d2RRU3JJVlBvaVh4eTlBRUtqWkJia3dJQVVBV2Nqdm9FUTRUVW1TaHp2ZUwxT0N2ZndxQ2Nka1RYWXF0LWxIWFE0dTFQcVhncFFPM0hFdUUtYlFnemx3WkF4bjA1aDFULUdrZlVZbEJtRGRCdjJyVkdJSXozd0I0dF9zbWhOeHFqRDA4T1NVaWR5cjBwSVgwbllPU294NjZGTnM1bFhIdGpNQUxFOENWd3FCbGpSRFRmRXotQnU0N2lCVEU5RGF6Qi10S2U2NGdadDlrRjZtVE5oZkw5ZWFjXzhCTmxXQzNFTFgxRXVYY3J3YkxnbnlBSm9PY3h4MlM1NVFQbVNDRW5Ld1dvNWMxSmdoTXJuaE1pT2VFeXYwWXBHZ29MZDVlN2lwUUNIeGNCVVdQVi1rRXdJMWFncUlPTXR0MmZVQ1l0d09mZTdzWGFBWUJMUFd3b0RSOU8zeER2UWpNdzAxS0ZJWnB5S3FJdU9wUDJnTTNwMWw3VFVqVXQ3ZGZnU1RkUktkc0NhUHJ0SGFxZ0lVWDEzYjNtU2JfMGNWM1Y0dHlCTzNESEdENC1jUWF5MVppRzR1QlBNSUJySjFfRi1ENHEwcmJ4S3hQUFpXVHA0TG9DZWdoUlo5WnNSM1lCZm1KbEs2ak1yUUU4Wk9JcVJGUkJwc0NvUkMyTjhoTWxtZmVQeDREZVRKZkhYN2duLVNTeGZzdFdBVnhEandJSXB5QjM0azF0ckI3Tk1wSzFhNGVOUVRrNjU0cG9JQ29pN09xOFkwR1lMTlktaGp4TktxdTVtTnNEcldsV2pEZm5nQWpJc2hxY0hjQnVSWUR5VVdaUXBHWUloTzFZUC1oNzJ4UjZ1dnpLcDJxWEZtQlNIMWkzZ0hXWXdKeC1iLXdZWVJhcU04VFlpMU5pd2ZIdTdCdkVWVFVBdmJuRk16bEFFQTh4alBrcTV2RzliT2hGdTVPOXlRMjFuZktiRTZIamQ1VFVqS0hRTXhxcU1mdkgyQ1NjQmZfcjl4c3NJd0RIeDVMZUFBbHJqdEJxWWl3aWdGUEQxR3ZnMkNGdVB4RUxkZi1xOVlFQXh1NjRfbkFEaEJ5TVZlUGFrWVhSTVRPeGxqNlJDTHNsRWRrei1pYjhnUmZrb3BvWkQ2QXBzYjFHNXZoWU1LSExhLWtlYlJTZlJmYUM5Y1Rhb1pkMVYyWTByM3NTS0VXMG1ybm1BTVN2QXRYaXZqX2dKSkZrajZSS2cyVlNOQnd5Y29zMlVyaWlNbTJEb3FuUFFtbWNTNVpZTktUenFZSl91cVFXZjRkQUZyYmtPczU2S1RKQ19ONGFOTHlwX2hOOEE1UHZEVjhnT0xxRjMxTEE4SHhRbmlmTkZwVXJBdlJDbU5oZS05SzI4QVhEWDZaN2ZiSlFwUGRXSnB5TE9MZV9ia3pYcmZVa1dicG5FMHRXUFZXMWJQVDAwOEdDQzJmZEl0ZDhUOEFpZXZWWXl5Q2xwSmFienNCMldlb2NKb2ZRYV9KbUdHRzNUcjU1VUFhMzk1a2J6dDVuNTl6NTdpM0hGa3k0UWVtbF9pdDVsQVp2cndDLUU5dnNYOF9CLS0ySXhBSFdCSnpqV010bllBb3U0cEZZYVF5R2tSNFM5NlRhdS1fb1NqbDBKMkw0V2N0VEZhNExtQlR3ckZ3cVlCeHVXdXJ6X0s4cEtsaG5rVUxCN2RRbHQxTmcyVFBqYUxyOHJzeFBXVUJaRHpXbUoxdHZzMFBzQk1UTUFvX1pGNFNMNDFvZWdTdEUtMUNKMXNIeVlvQk1CeEdpZVdmN0tsSDVZZHJXSGt5c2o2MHdwSTZIMVBhRzM1eU43Q2FtcVNidExxczNJeUx5U2RuUG5EeHpCTlg2SV9WNk1ET3BRNXFuc0pNWlVvZUYtY21oRGtJSmwxQ09QbHBUV3BuS3B5NE9RVkhfellqZjJUQ0diSV94QlhQWmdaaC1TRWxsMUVWSXB0aE1McFZDZDNwQUVKZ2t5cXRTXzlRZVJwN0pZSnJSV21XMlh0TzFRVEl0c2I4QjBxOGRCYkNxek04a011X1lrb2poQ3h2LUhKTGJiUlhneHp5QWFBcE5nMElkNTVzM3JGOWtUQ19wNVBTaVVHUHFDNFJnNXJaWDNBSkMwbi1WbTdtSnFySkhNQl9ZQjZrR2xDcXhTRExhMmNHcGlyWjR3ZU9SSjRZd1l4ZjVPeHNiYk53SW5SYnZPTzNkd1lnZmFseV9tQ3BxM3lNYVBHT0J0elJnMTByZ3VHemxta0tVQzZZRllmQ2VLZ1ZCNDhUUTc3LWNCZXBMekFwWW1fQkQ1NktzNGFMYUdYTU0xbXprY1FONUNlUHNMY3h2NFJMMmhNa3VNdzF4TVFWQk9odnJUMjFJMVd3Z2N6Sms5aEM2SWlWZFViZ0JWTEpUWWM5NmIzOS1oQmRqdkt1NUUycFlVcUxERUZGbnZqTUxIYnJmMDBHZDEzbnJsWEEzSUo3UmNPUDg1dnRUU1FzcWtjTWZwUG9zM0JTY3RqMDdST2UxcXFTM0d0bGkwdFhnMk5LaUlxNWx3V1pLaVlLUFJXZzBzVl9Ia1V1OHdYUEFWOU50UndycGtCdzM0Q0NQamp2VTNqbFBLaGhsbUk5dUI5MjU5OHVySk1oY0drUWtXUloyVVRvOWJmbUVYRzFVeWNQczh2NXJCeVppRlZiWDNJaDhOSmRmX2lURTNVS3NXQXFZT1QtUmdvMWJoVWYxU3lqUUJhbzEyX3I3TXhwbm9wc1FoQ1ZUTlNBRjMyQTBTY2tzbHZ3RFUtTjVxQ0o1QXRTVks2WENwMGZCRGstNU1jN3FhUFJCQThyaFhhMVRsbnlSRXNGRmt3Yk01X21ldmV3bTItWm1JaGpZQWZROEFtT1d1UUtPQlhYVVFqT2NxLUxQenJHX3JfMEdscDRiMXcyZ1ZmU3NFMzVoelZJaDlvT0ZoRGQ2bmtlM0M5ZHlCd2ZMbnRZRkZUWHVBUEx4czNfTmtMckh5eXZrZFBzOEItOGRYOEhsMzBhZ0xlOWFjZzgteVBsdnpPT1pYdUxnbFNXYnhKaVB6QUxVdUJCOFpvU2x2c1FHZV94MDBOVWJhYkxISkswc0U5UmdPWFJLXzZNYklHTjN1QzRKaldKdEVHb0pOU284N3c2LXZGMGVleEZ5NGZ6OGV1dm1tM0J0aTQ3VFlNOEJrdEh3PT0=
diff --git a/env_prod.env b/env_prod.env
index 67092526..e3cdfcc2 100644
--- a/env_prod.env
+++ b/env_prod.env
@@ -50,23 +50,11 @@ APP_LOGGING_BACKUP_COUNT = 5
Service_MSFT_REDIRECT_URI = https://gateway-prod.poweron-center.net/api/msft/auth/callback
Service_GOOGLE_REDIRECT_URI = https://gateway-prod.poweron-center.net/api/google/auth/callback
-# OpenAI configuration
-Connector_AiOpenai_API_URL = https://api.openai.com/v1/chat/completions
+# AI configuration
Connector_AiOpenai_API_SECRET = PROD_ENC:Z0FBQUFBQm8xSU5pU05XM2hMaExPMnpYeFpwRVhyYl9JZmRITmlmRDlWOUJSSWE4NTFLZUptSkJhNlEycHBLZmh3WFA2ZmU5VmxHZks1UUNVOUZnckZNdXZ2MTY2dFg1Nl8yWDRrcTRlT0tHYkhyRGZINTEzU25iYVFRMzJGeUZIdlc4LU9GbmpQYmtmU3lJT2VVZ1UzLVd3R25ZQ092SUVnPT0=
-Connector_AiOpenai_MODEL_NAME = gpt-4o
-Connector_AiOpenai_TEMPERATURE = 0.2
-
-# Anthropic configuration
-Connector_AiAnthropic_API_URL = https://api.anthropic.com/v1/messages
Connector_AiAnthropic_API_SECRET = PROD_ENC:Z0FBQUFBQm8xSU5pNTA1RkZ3UllCOXVsNVZzbkw2Rkl1TWxCZ0wwWEVXUm9ReUhBcVl1cGFUdW9FRVh4elVxR0x3NVRxZkc4SkxHVFdzSU1YNG5Rb0FqSHJhdElwWm1iLWdubTVDcUl3UkVjVHNoU0xLa0ZTSFlfTlJUVXg4cVVwUWdlVDBTSFU5SnBzS0ZnVjlQcmtiNzV2UTNMck1IakZ0OWlubUtlWDZnMk4yX2JsZ1U4Wm1yT29fM2d2NVBNOWNBbWtTRWNyQ2tZNjhwSVF6bG5SU3dTenR2MzA3Z19NUT09
-Connector_AiAnthropic_MODEL_NAME = claude-3-5-sonnet-20241022
-Connector_AiAnthropic_TEMPERATURE = 0.2
-
-# Perplexity AI configuration
-Connector_AiPerplexity_API_URL = https://api.perplexity.ai/chat/completions
Connector_AiPerplexity_API_SECRET = PROD_ENC:Z0FBQUFBQm82Mzk2Q1FGRkJEUkI4LXlQbHYzT2RkdVJEcmM4WGdZTWpJTEhoeUF1NW5LUVpJdDBYN3k1WFN4a2FQSWJSQmd0U0xJbzZDTmFFN05FcXl0Z3V1OEpsZjYydV94TXVjVjVXRTRYSWdLMkd5XzZIbFV6emRCZHpuOUpQeThadE5xcDNDVGV1RHJrUEN0c1BBYXctZFNWcFRuVXhRPT0=
-Connector_AiPerplexity_MODEL_NAME = sonar
-Connector_AiPerplexity_TEMPERATURE = 0.2
+Connector_AiTavily_API_SECRET = PROD_ENC:Z0FBQUFBQm8xSU5pMjhJNS1CZFJubUlkN3ZrTUoxR0Y1QzJFWEJSMk0wQkI0UndqOW1UelVieWhGaTVBcHoxRXo1VjRzVVRROHFIeHMyS3Q5cDZCeUlEMzE1ZlhVTmNveFk5VmFQMm80NTRyVW1TZHVsR3dUN0RtMnd4LW1VWlpqOXJPeXZBTmg4OEM=
# Agent Mail configuration
Service_MSFT_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
@@ -77,9 +65,6 @@ Service_MSFT_TENANT_ID = common
Service_GOOGLE_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQm8xSU5pV2JEV0lNUXhwa1VTUGh2RWcyYnJHSFQyTmdBOEhwRkJWc3MwOFZlcHJGUmlGOVVFbG1XalNyUXVuaExESy1xeFNIQlRiSFVIWTB6Rm1fNFg0OHZZSkF4ZlBIcFZDMjZHcFRERXJ0WlVFclhHa29Za1BqWGxsM05NZGFRc1BLZnE=
-# Tavily Web Search configuration
-Connector_WebTavily_API_KEY_SECRET = PROD_ENC:Z0FBQUFBQm8xSU5pMjhJNS1CZFJubUlkN3ZrTUoxR0Y1QzJFWEJSMk0wQkI0UndqOW1UelVieWhGaTVBcHoxRXo1VjRzVVRROHFIeHMyS3Q5cDZCeUlEMzE1ZlhVTmNveFk5VmFQMm80NTRyVW1TZHVsR3dUN0RtMnd4LW1VWlpqOXJPeXZBTmg4OEM=
-
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = PROD_ENC:Z0FBQUFBQm8xSU5pNjlJdmFMeERXUUQzR0duRUY4cGRZRzdwQlpnVFAzSzQ5cHZNRnVUZ0xWd3dQMHR3QjVsdF92NmdUQlJGRk1RcG1RYWZzcE9RbEhjQmR5Yk5Ud3ZKTW5jbmpEVGJ2ZkxVeVJpcUxaT2lNREFXaks5WHg5aVlHcXlUZldMdnZGYklHWjlJOWJ6Wm5RSkNmdm5feENjS1E0QUVXTTE5SW5sNFBEeTJ1RjRmVm9SQUNIYmF2U1U2dklsbTVlWFpCcHMwTFF1SUg5NmNfcWhQRFlpeWt0U19HMXNuUHd2RFdrVl9XdUFaY0hWdVBPYWlybU1CdGlCN1A0RzZBbi1IUVJ1TWMxTE9Ea09sTURhcDFZb1JIUW1zUFJybW15MDcxOUtfVXA2N0xwMnFrczA1YTJaN05pRHhOYWNzMjVmUHdhbVdlemF3TEIzN0pJaVo3bGJBMXJnZmNYTXVJVDdmYkRXWTlBT2F2NmN4eTlteUI1SlJTOXc2WWFWUTBCZTJBVHRLVDhEVjBFeHE0Nmk1YkxYd3N3RXgtVUdGdlZFSmk4dHM0QjFmbktsQTctbmJMT0MtMDlKS1pUR0pELXBxckhULUUycjlBZmVJQjFrM0xEUm50U2ZabExtVjZ1WWZ1WnlobUZIOVlndjNydUZfczJUWVVRZURTd1lYazllaER4VU10cXUyVS1ZNG9Ha2hnbTAzOEpGMklFSWpWeVV5eFB2UlVWYmJJakZnOVM2R2lJSXRSM3VzVEZZNUVpNmVjRzdXRUJsT2hzcjhZWERFeGV5c1dFQVM3dkhGY2Q3ckNBRDZCcVdhZnZkdzM3QVNpODZYWE81TEIyZGUycldkSVRvbm5hR3Jib2UzOEtXdUpHQ2FyWDQtMDdQbC1ycEdfUzdXd0U2dHFIVjhoRDJ0YkNsWUpva1dzOGNPdXRpZjVwUldtT3FVN3RrZUhTN3JfX1M3LU9PaXZELWkzRmtMbjgxZGZ6ZjVJNW9RZW1nM2hqUXo4Z2I5Z2tSVTVMdUNLblRxOGQ1Y3F4SGZIbWo4YkFBV3FIbjB6LUxGNHdsQWgxQUM4bzVrblBObFFfVWNaQ3QwejQ1eGFlSXVIcXlyVEZEdzVKNV9pd2o4RW1UVjlqb3VMWnF0V1JTcWF1R0RjdUNjM2lLUHRqZDl2WWtXUnhmbVdxeHA3REFHTkdkMjM4LTllajBWQnd3RHlFSVdiUThfQnduOVFJdmR6OUVGN1lOYjBqclhadHozX21kRzlUT2EtWVBkYWFRSjRGdW80dmlEUTVrVjhWbjJYNGtCeGNtNzRHQXJsRlZyWjBYdHltVDM2MV9IT0RFT2dLLTVBREtsS09HdUxrODRLcEQ1TmRoVDh6WmgybGc5MzgtbmJSYThQd3FFaUcxbmg3eE95RkJVX2hHM20wT1k2c21qd24wSkFWNGROaklQeHZrc21PdTVsdHVxR0pxd3Ztb1NQVHEtd25URHRNa1pqa3BLdVdkTnNFeDNManJST0dOb1RWM2hqekxFTlFSZkd6TlZBY1VQT1NFOVlDQzlPQWVlVXQ4MW0wdGkzd0Myam1lSWE2aEtVVTVNc3N3dENpa1BWRl9ZQ3daYllONWRmRUF0THpleFRmdWRqTFM2aldmLUFuZzFGdkFQNHR6d21SdzRGQ0Q4cU8yV0xGUTVUY01TZlYxSzZ4cmtfUGZvVDhmYmNBX1pibTVTcl9lenJoME9KSnBucUxPRU1PRXBmLWFENEgwRWZOU0RvRDlvQk9ueVp0dXJrUVgtQUk5VldVbV9MS19PYmlua3liWl80Z2hMcFRnTXBkZDA3enIxRWFzaU56TEZKa0hPQUtNY0dCY1pnQ2V3Zml6ZFczWFBESUlLd3BSVEs5ZXlGLUpINDRsd1NBVjBkR1dvbE8wLWZBeEhFQ0hvY3E5UGJsTDdteGdSRjBIZTRobXpsd29PMmhKQkxXY3Znd2FMdWtZU1VkQlVRZXlSZ3FaVnNqcXpwR3N3SktOTDA3aUZIcE9TR1VDcXdaTDhQX2E5VDlwckoyX0xlNmFQcnoydEkwc0s1S08yaVlsM0pwYktUVWl3LU5hQzF2UVZNSm9ZR3QyQWdrUXB2a25QNzhkVEFOYmZ0b1BmTXRCMmVQZTAtYzdOeUlBYlNINlZNZW1nUTFfSV92UlJiWGt6Qms1c1hBc3kzZkVRMzEwNVJDOS1JeVg4YWtVeUJyOTZPQ0FnSUs1Z25sMlY0S1V1c0dIWEpuX2pMQmZ4Z29SY1U0bVZscXNWcjJwRy1UZEFYSXBzQURGblRTelBybU5BeDF6N3hZLXZwSHBkMmlzbHZWN2JkU3hRcE0zQ0hna3QwYWlJX3hBdGcxUHdGRE55cndUNHRvbXU5VTRMRmZDRjhvXzIwajI1Y0RCcmR2OV94cS1XYkNwalNHS2lObHlkNGZBbklycnZMSlJYVnlfakRXb1ZfWUo2MGxzYUNIektYeENGTkUzMUJXRE9WRHRrY2o5UFJHckZza2RQbjNPUkstbG9GZG4yNmxKeEdtbHo4WDZFc0lvT01wZkxuN29ycXl3X1hTN1prRGdvWG9hRFYwNzBwVVpuMW0wQlZYbGZxZjFQUHp2XzBQT3Fqa3lzejVKZmJDMG0wRzhqWV9HY1dxaXB2VFNQUzV2LUJSOXRFRUllak83cUI3RGUtYVBJakF1YUVOV0otT1BxUHJqS0NLdFVHc0tsT2RGcWd6UTU4Yi1kc0JZS1VPT1NXSlc3TDM5ZDVEZlRDOURZU1hMT0YxZ25ndVBUaG1VcGsxWFZSS1RxT1ZZTU1vclZjVU5iYmZMd0VBTXlvdTE0YjdoclZ6ZnNKMmE2Yy1ORmNCMnJNX3dwcVJSN2RSd2d6aENLRXQyTjhkcDlLTFVZMHBydFowNTJoZm1mVHNRVHI1YjhTNnl1Vll4dFZhenZfa0dybk9KYVh6LUluSUo0djUzRFNEdzBoVGt5UU9tMlg5UnBLbk9WaEhoU2txY2tUSXJmemlmNEExb3Q1blI5bE9adHluWVI3NXZQNUtXdmpra05aNy15dTBXdlVqcXhteFVqSXFxNnlQR2FGeVNONkx3NVpQUk1FNk5yTUY4T1hQV1FCdm9PYzdFTGl4QXZkODltSlprbGJ6cWREcEM1VlNwN3V5aWdWYXNkekk4X3U0cjJjZ1k2X190cmNnMlpMQVlLdExxM3pFNkZudVFKci1CalE1U3kzdmotQ01LV0ZzWnp0VUxRblhkdlN6VG1MWHNQdGlrNmF4RnFtd0c3UXNqZFVRZTRFMGl1NFU5T2k3VEpjZXA1U052VkJtdUhDWEpTaDRGQnM0SDQwY2IxdDVNbUtELTQ0R0s0OHpfTHdFOHZ0VmRMTC1FUVpPSkJ4QXRWNnl5MURUdjVyUk53emRwbDBxUnloUmlheXhKY3RBUG1mX3JxM2w0VlZvcE40b2ROeG15NS01RFlvUHdoYllLNVhCZUNEd0dwQnFCLVdZU0RhVEFzR2gxTVpub3FGRnl4VDNiSVZrTnpMQUlxeGJGQzh5WlNZR2NKbklHRVRTaVJ2REduN0hXaGo5MHFGb1FOa0U5TUFwQ09zOXVWMnRRNVlJWmZpaTUxLWFIeWR0UEFtaVNDX1k5Q1p3Y2V4ckVXQVBRYzV1eGwwMWd0SE15WUxiYzUyLTUzTGlyTUhZUDFlRTFjcFpieWQwU0pxRWJXSE53Nkd5aHp5T28wZVd6Z1phLTQ4TmgxU3hvNHpySzExUk5WZlFFS3VpOXNHMDdZU0gzSGxYUlU4WmgwNUlPdlhQcUI0cGtITmQ4SlByczN0THUxNHc0a21vUEp6S1hLNnFRNmFfdlpmUWpJQ1VNYXVEOW1abzlsd2RoRG5pVXRVbjBKV2RFTGFEa3ZYTHByOTJjalc1b3hTWkFmS2RPdVlTUTVkRkpSTnZsMWtnYWZEUm1SR3lBemdON2xiN3pkZlNfX2NSYU5wWHNybHh4V0lnNHJjQ2NON1hiRHMycUdmNC1kay13bUE0OTBPN0xmNDA1NlQxVmRySEJvM1VUN2Y2Sl9KX2pZVHRPWEdfR2RYNUoxY01Va3pXb2VBd3lZb3BSXzU5NVJfWlhEYXFSVDJrUnFHWG42RVZJUVQ2RlJWUEkyQnRnREI3eHNiRERiQ3FUczJsRTBDZ3pUUGZPcjExZUFKc21QUWxVYVBmV2hPZXRGd3lJX3ZTczhCVG1jWFVwanhIZHlyTTdiR2c5cTBVSXBRV1U4ZExtWWdub1pTSHU0cU5aYWJVWmExbXI0MjE3WUVnPT0=
diff --git a/modules/aicore/aicoreBase.py b/modules/aicore/aicoreBase.py
index 8cca3dfd..b3d36dd9 100644
--- a/modules/aicore/aicoreBase.py
+++ b/modules/aicore/aicoreBase.py
@@ -66,10 +66,6 @@ class BaseConnectorAi(ABC):
return model
return None
- def getModelsByCapability(self, capability: str) -> List[AiModel]:
- """Get models that support a specific capability."""
- models = self.getCachedModels()
- return [model for model in models if capability in model.capabilities]
def getModelsByPriority(self, priority: str) -> List[AiModel]:
"""Get models that have a specific priority."""
diff --git a/modules/aicore/aicoreModelRegistry.py b/modules/aicore/aicoreModelRegistry.py
index a3666114..b243d876 100644
--- a/modules/aicore/aicoreModelRegistry.py
+++ b/modules/aicore/aicoreModelRegistry.py
@@ -112,10 +112,6 @@ class ModelRegistry:
self.refreshModels()
return [model for model in self._models.values() if model.connectorType == connectorType]
- def getModelsByCapability(self, capability: str) -> List[AiModel]:
- """Get models that support a specific capability."""
- self.refreshModels()
- return [model for model in self._models.values() if capability in model.capabilities]
def getModelsByPriority(self, priority: str) -> List[AiModel]:
"""Get models that have a specific priority."""
diff --git a/modules/aicore/aicoreModelSelector.py b/modules/aicore/aicoreModelSelector.py
index 76674ed8..dfc5e118 100644
--- a/modules/aicore/aicoreModelSelector.py
+++ b/modules/aicore/aicoreModelSelector.py
@@ -71,8 +71,13 @@ class ModelSelector:
contextSize = len(context.encode("utf-8"))
totalSize = promptSize + contextSize
- # Step 1: Filter by operation type (MUST match)
- operationFiltered = [m for m in availableModels if options.operationType in m.operationTypes]
+ # Step 1: Filter by operation type (MUST match) - check if model has this operation type
+ operationFiltered = []
+ for model in availableModels:
+ # Check if model has the required operation type
+ hasOperationType = any(ot.operationType == options.operationType for ot in model.operationTypes)
+ if hasOperationType:
+ operationFiltered.append(model)
logger.debug(f"After operation type filtering: {len(operationFiltered)} models")
# Step 2: Filter by prompt size (MUST be <= 80% of context size)
@@ -100,6 +105,7 @@ class ModelSelector:
def _calculateModelScore(self, model: AiModel, promptSize: int, contextSize: int, totalSize: int, options: AiCallOptions) -> float:
"""
Calculate a score for a model based on how well it fulfills the criteria.
+ Operation type rating is the PRIMARY sorting criteria (multiplied by 1000).
Args:
model: The model to score
@@ -113,7 +119,11 @@ class ModelSelector:
"""
score = 0.0
- # 1. Prompt + Context size rating
+ # 1. PRIMARY: Operation Type Rating (multiplied by 1000 for primary sorting)
+ operationTypeRating = self._getOperationTypeRating(model, options.operationType)
+ score += operationTypeRating * 1000.0 # Primary sorting criteria
+
+ # 2. Prompt + Context size rating
if model.contextLength > 0:
modelMaxSize = model.contextLength * 0.8 # 80% of model context length
if totalSize <= modelMaxSize:
@@ -126,13 +136,13 @@ class ModelSelector:
# No context length limit
score += 1.0
- # 2. Processing Mode rating
+ # 3. Processing Mode rating
if hasattr(options, 'processingMode') and options.processingMode:
score += self._getProcessingModeRating(model.processingMode, options.processingMode)
else:
score += 1.0 # No preference
- # 3. Priority rating
+ # 4. Priority rating
if hasattr(options, 'priority') and options.priority:
score += self._getPriorityRating(model, options.priority)
else:
@@ -140,6 +150,22 @@ class ModelSelector:
return score
+ def _getOperationTypeRating(self, model: AiModel, operationType: OperationTypeEnum) -> float:
+ """
+ Get the operation type rating for a model.
+
+ Args:
+ model: The model to check
+ operationType: The operation type to get rating for
+
+ Returns:
+ Rating (1-10) or 0 if model doesn't support this operation type
+ """
+ for ot_rating in model.operationTypes:
+ if ot_rating.operationType == operationType:
+ return float(ot_rating.rating)
+ return 0.0 # Model doesn't support this operation type
+
def _getProcessingModeRating(self, modelMode: ProcessingModeEnum, requestedMode: ProcessingModeEnum) -> float:
"""Get processing mode rating based on compatibility."""
if modelMode == requestedMode:
@@ -215,10 +241,10 @@ class ModelSelector:
logger.info(f" Quality Rating: {model.qualityRating}/10")
logger.info(f" Speed Rating: {model.speedRating}/10")
logger.info(f" Cost: ${model.costPer1kTokensInput:.4f}/1k tokens")
- logger.info(f" Capabilities: {', '.join(model.capabilities)}")
logger.info(f" Priority: {model.priority}")
logger.info(f" Processing Mode: {model.processingMode}")
- logger.info(f" Operation Types: {', '.join(model.operationTypes)}")
+ operationTypesStr = ', '.join([f"{ot.operationType.value}({ot.rating})" for ot in model.operationTypes])
+ logger.info(f" Operation Types: {operationTypesStr}")
# Global model selector instance
diff --git a/modules/aicore/aicorePluginAnthropic.py b/modules/aicore/aicorePluginAnthropic.py
index b5d552cd..5f5e2bcb 100644
--- a/modules/aicore/aicorePluginAnthropic.py
+++ b/modules/aicore/aicorePluginAnthropic.py
@@ -5,7 +5,7 @@ from typing import Dict, Any, List, Union
from fastapi import HTTPException
from modules.shared.configuration import APP_CONFIG
from modules.aicore.aicoreBase import BaseConnectorAi
-from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse
+from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse, createOperationTypeRatings
# Configure logger
logger = logging.getLogger(__name__)
@@ -14,9 +14,6 @@ def loadConfigData():
"""Load configuration data for Anthropic connector"""
return {
"apiKey": APP_CONFIG.get('Connector_AiAnthropic_API_SECRET'),
- "apiUrl": APP_CONFIG.get('Connector_AiAnthropic_API_URL'),
- "modelName": APP_CONFIG.get('Connector_AiAnthropic_MODEL_NAME'),
- "temperature": float(APP_CONFIG.get('Connector_AiAnthropic_TEMPERATURE')),
}
class AiAnthropic(BaseConnectorAi):
@@ -27,8 +24,6 @@ class AiAnthropic(BaseConnectorAi):
# Load configuration
self.config = loadConfigData()
self.apiKey = self.config["apiKey"]
- self.apiUrl = self.config["apiUrl"]
- self.modelName = self.config["modelName"]
# HttpClient for API calls
self.httpClient = httpx.AsyncClient(
@@ -40,7 +35,7 @@ class AiAnthropic(BaseConnectorAi):
}
)
- logger.info(f"Anthropic Connector initialized with model: {self.modelName}")
+ logger.info("Anthropic Connector initialized")
def getConnectorType(self) -> str:
"""Get the connector type identifier."""
@@ -50,38 +45,49 @@ class AiAnthropic(BaseConnectorAi):
"""Get all available Anthropic models."""
return [
AiModel(
- name="anthropic_callAiBasic",
- displayName="Claude 3.5 Sonnet",
+ name="claude-3-5-sonnet-20241022",
+ displayName="Anthropic Claude 3.5 Sonnet",
connectorType="anthropic",
+ apiUrl="https://api.anthropic.com/v1/messages",
+ temperature=0.2,
maxTokens=200000,
contextLength=200000,
costPer1kTokensInput=0.015,
costPer1kTokensOutput=0.075,
- speedRating=7,
- qualityRating=10,
- capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.ANALYSIS],
+ speedRating=6, # Slower due to high-quality processing
+ qualityRating=10, # Best quality available
+ # capabilities removed (not used in business logic)
functionCall=self.callAiBasic,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
- operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.PLAN, OperationTypeEnum.ANALYSE, OperationTypeEnum.GENERATE],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.PLAN, 9),
+ (OperationTypeEnum.DATA_ANALYSE, 10),
+ (OperationTypeEnum.DATA_GENERATE, 9),
+ (OperationTypeEnum.DATA_EXTRACT, 8)
+ ),
version="claude-3-5-sonnet-20241022",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
),
AiModel(
- name="anthropic_callAiImage",
- displayName="Claude 3.5 Sonnet Vision",
+ name="claude-3-5-sonnet-20241022-vision",
+ displayName="Anthropic Claude 3.5 Sonnet Vision",
connectorType="anthropic",
+ apiUrl="https://api.anthropic.com/v1/messages",
+ temperature=0.2,
maxTokens=200000,
contextLength=200000,
costPer1kTokensInput=0.015,
costPer1kTokensOutput=0.075,
- speedRating=7,
- qualityRating=10,
- capabilities=[ModelCapabilitiesEnum.IMAGE_ANALYSE, ModelCapabilitiesEnum.VISION, ModelCapabilitiesEnum.MULTIMODAL],
+ speedRating=6, # Slower due to high-quality processing
+ qualityRating=10, # Best quality available
+ # capabilities removed (not used in business logic)
functionCall=self.callAiImage,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
- operationTypes=[OperationTypeEnum.IMAGE_ANALYSE],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.IMAGE_ANALYSE, 10)
+ ),
version="claude-3-5-sonnet-20241022",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
)
@@ -106,7 +112,7 @@ class AiAnthropic(BaseConnectorAi):
messages = modelCall.messages
model = modelCall.model
options = modelCall.options
- temperature = options.get("temperature", self.config.get("temperature", 0.2))
+ temperature = options.get("temperature", model.temperature)
maxTokens = model.maxTokens
# Transform OpenAI-style messages to Anthropic format:
@@ -148,7 +154,7 @@ class AiAnthropic(BaseConnectorAi):
# Create Anthropic API payload
payload: Dict[str, Any] = {
- "model": self.modelName,
+ "model": model.name,
"messages": converted_messages,
"temperature": temperature,
}
@@ -161,7 +167,7 @@ class AiAnthropic(BaseConnectorAi):
payload["system"] = system_prompt
response = await self.httpClient.post(
- self.apiUrl,
+ model.apiUrl,
json=payload
)
@@ -207,7 +213,7 @@ class AiAnthropic(BaseConnectorAi):
return AiModelResponse(
content=content,
success=True,
- modelId=self.modelName,
+ modelId=model.name,
metadata={"response_id": anthropicResponse.get("id", "")}
)
@@ -215,19 +221,25 @@ class AiAnthropic(BaseConnectorAi):
logger.error(f"Error calling Anthropic API: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error calling Anthropic API: {str(e)}")
- async def callAiImage(self, prompt: str, imageData: Union[str, bytes], mimeType: str = None) -> str:
+ async def callAiImage(self, modelCall: AiModelCall) -> AiModelResponse:
"""
- Analyzes an image using Anthropic's vision capabilities.
+ Analyzes an image using Anthropic's vision capabilities using standardized pattern.
Args:
- imageData: Either a file path (str) or image data (bytes)
- mimeType: The MIME type of the image (optional, only for binary data)
- prompt: The prompt for analysis
+ modelCall: AiModelCall with messages and image data in options
Returns:
- The analysis response as text
+ AiModelResponse with analysis content
"""
try:
+ # Extract parameters from modelCall
+ messages = modelCall.messages
+ model = modelCall.model
+ options = modelCall.options
+ prompt = messages[0]["content"] if messages else ""
+ imageData = options.get("imageData")
+ mimeType = options.get("mimeType")
+
# Debug logging
logger.info(f"callAiImage called with imageData type: {type(imageData)}, length: {len(imageData) if imageData else 0}, mimeType: {mimeType}")
@@ -272,20 +284,22 @@ class AiAnthropic(BaseConnectorAi):
}
]
- # Use the existing callAiBasic function with the Vision model
- response = await self.callAiBasic(messages)
+ # Create a modelCall for the basic AI function
+ basicModelCall = AiModelCall(
+ messages=messages,
+ model=model
+ )
- # Extract and return content with proper error handling
- try:
- content = response["choices"][0]["message"]["content"]
- if content is None or content.strip() == "":
- return "[AI returned empty response for image analysis]"
- return content
- except (KeyError, IndexError, TypeError) as e:
- logger.error(f"Error extracting content from AI response: {str(e)}")
- logger.error(f"Response structure: {response}")
- return f"[Error extracting AI response: {str(e)}]"
+ # Use the existing callAiBasic function with the Vision model
+ response = await self.callAiBasic(basicModelCall)
+
+ # Return the standardized response
+ return response
except Exception as e:
logger.error(f"Error during image analysis: {str(e)}", exc_info=True)
- return f"[Error during image analysis: {str(e)}]"
\ No newline at end of file
+ return AiModelResponse(
+ content="",
+ success=False,
+ error=f"Error during image analysis: {str(e)}"
+ )
\ No newline at end of file
diff --git a/modules/aicore/aicorePluginInternal.py b/modules/aicore/aicorePluginInternal.py
index b121f595..bb28de50 100644
--- a/modules/aicore/aicorePluginInternal.py
+++ b/modules/aicore/aicorePluginInternal.py
@@ -1,7 +1,7 @@
import logging
from typing import Dict, Any, List, Union
from modules.aicore.aicoreBase import BaseConnectorAi
-from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse
+from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse, createOperationTypeRatings
# Configure logger
logger = logging.getLogger(__name__)
@@ -21,56 +21,62 @@ class AiInternal(BaseConnectorAi):
"""Get all available internal models."""
return [
AiModel(
- name="internal_extraction",
+ name="internal-extractor",
displayName="Internal Document Extractor",
connectorType="internal",
+ apiUrl="internal://extract",
+ temperature=0.0, # Not applicable for extraction
maxTokens=0, # Not token-based
contextLength=0,
costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0,
- speedRating=8,
- qualityRating=8,
- capabilities=[ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.TEXT_EXTRACTION],
+ speedRating=9, # Very fast for internal operations
+ qualityRating=8, # Good quality
+ # capabilities removed (not used in business logic)
functionCall=self.extractDocument,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
- operationTypes=[OperationTypeEnum.EXTRACT],
+ operationTypes=createOperationTypeRatings(),
version="internal-extractor-v1",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.001 + (bytesSent + bytesReceived) / (1024 * 1024) * 0.01
),
AiModel(
- name="internal_generation",
+ name="internal-generator",
displayName="Internal Document Generator",
connectorType="internal",
+ apiUrl="internal://generate",
+ temperature=0.0, # Not applicable for generation
maxTokens=0, # Not token-based
contextLength=0,
costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0,
- speedRating=7,
- qualityRating=8,
- capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.ANALYSIS],
+ speedRating=8, # Fast for generation
+ qualityRating=8, # Good quality
+ # capabilities removed (not used in business logic)
functionCall=self.generateDocument,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
- operationTypes=[OperationTypeEnum.GENERATE],
+ operationTypes=createOperationTypeRatings(),
version="internal-generator-v1",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.002 + (bytesReceived / (1024 * 1024)) * 0.005
),
AiModel(
- name="internal_rendering",
+ name="internal-renderer",
displayName="Internal Document Renderer",
connectorType="internal",
+ apiUrl="internal://render",
+ temperature=0.0, # Not applicable for rendering
maxTokens=0, # Not token-based
contextLength=0,
costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0,
- speedRating=6,
- qualityRating=9,
- capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.ANALYSIS],
+ speedRating=7, # Good for rendering
+ qualityRating=9, # High quality rendering
+ # capabilities removed (not used in business logic)
functionCall=self.renderDocument,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
- operationTypes=[OperationTypeEnum.GENERATE],
+ operationTypes=createOperationTypeRatings(),
version="internal-renderer-v1",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.003 + (bytesReceived / (1024 * 1024)) * 0.008
)
diff --git a/modules/aicore/aicorePluginOpenai.py b/modules/aicore/aicorePluginOpenai.py
index 849a4600..49667903 100644
--- a/modules/aicore/aicorePluginOpenai.py
+++ b/modules/aicore/aicorePluginOpenai.py
@@ -5,7 +5,7 @@ from typing import Dict, Any, List, Union
from fastapi import HTTPException
from modules.shared.configuration import APP_CONFIG
from modules.aicore.aicoreBase import BaseConnectorAi
-from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse
+from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse, createOperationTypeRatings
# Configure logger
logger = logging.getLogger(__name__)
@@ -18,9 +18,6 @@ def loadConfigData():
"""Load configuration data for OpenAI connector"""
return {
"apiKey": APP_CONFIG.get('Connector_AiOpenai_API_SECRET'),
- "apiUrl": APP_CONFIG.get('Connector_AiOpenai_API_URL'),
- "modelName": APP_CONFIG.get('Connector_AiOpenai_MODEL_NAME'),
- "temperature": float(APP_CONFIG.get('Connector_AiOpenai_TEMPERATURE')),
}
class AiOpenai(BaseConnectorAi):
@@ -31,8 +28,6 @@ class AiOpenai(BaseConnectorAi):
# Load configuration
self.config = loadConfigData()
self.apiKey = self.config["apiKey"]
- self.apiUrl = self.config["apiUrl"]
- self.modelName = self.config["modelName"]
# HttpClient for API calls
self.httpClient = httpx.AsyncClient(
@@ -42,7 +37,7 @@ class AiOpenai(BaseConnectorAi):
"Content-Type": "application/json"
}
)
- logger.info(f"OpenAI Connector initialized with model: {self.modelName}")
+ logger.info("OpenAI Connector initialized")
def getConnectorType(self) -> str:
"""Get the connector type identifier."""
@@ -52,74 +47,95 @@ class AiOpenai(BaseConnectorAi):
"""Get all available OpenAI models."""
return [
AiModel(
- name="openai_callAiBasic",
- displayName="GPT-4o",
+ name="gpt-4o",
+ displayName="OpenAI GPT-4o",
connectorType="openai",
+ apiUrl="https://api.openai.com/v1/chat/completions",
+ temperature=0.2,
maxTokens=128000,
contextLength=128000,
costPer1kTokensInput=0.03,
costPer1kTokensOutput=0.06,
- speedRating=8,
- qualityRating=9,
- capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.ANALYSIS],
+ speedRating=7, # Good speed for complex tasks
+ qualityRating=9, # High quality
+ # capabilities removed (not used in business logic)
functionCall=self.callAiBasic,
priority=PriorityEnum.BALANCED,
processingMode=ProcessingModeEnum.ADVANCED,
- operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.PLAN, OperationTypeEnum.ANALYSE, OperationTypeEnum.GENERATE],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.PLAN, 8),
+ (OperationTypeEnum.DATA_ANALYSE, 9),
+ (OperationTypeEnum.DATA_GENERATE, 9),
+ (OperationTypeEnum.DATA_EXTRACT, 7)
+ ),
version="gpt-4o",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
),
AiModel(
- name="openai_callAiBasic_gpt35",
- displayName="GPT-3.5 Turbo",
+ name="gpt-3.5-turbo",
+ displayName="OpenAI GPT-3.5 Turbo",
connectorType="openai",
+ apiUrl="https://api.openai.com/v1/chat/completions",
+ temperature=0.2,
maxTokens=16000,
contextLength=16000,
costPer1kTokensInput=0.0015,
costPer1kTokensOutput=0.002,
- speedRating=9,
- qualityRating=7,
- capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING],
+ speedRating=9, # Very fast
+ qualityRating=7, # Good but not premium
+ # capabilities removed (not used in business logic)
functionCall=self.callAiBasic,
priority=PriorityEnum.SPEED,
processingMode=ProcessingModeEnum.BASIC,
- operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.PLAN, OperationTypeEnum.GENERATE],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.PLAN, 7),
+ (OperationTypeEnum.DATA_ANALYSE, 8),
+ (OperationTypeEnum.DATA_GENERATE, 8)
+ ),
version="gpt-3.5-turbo",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0015 + (bytesReceived / 4 / 1000) * 0.002
),
AiModel(
- name="openai_callAiImage",
- displayName="GPT-4o Vision",
+ name="gpt-4o-vision",
+ displayName="OpenAI GPT-4o Vision",
connectorType="openai",
+ apiUrl="https://api.openai.com/v1/chat/completions",
+ temperature=0.2,
maxTokens=128000,
contextLength=128000,
costPer1kTokensInput=0.03,
costPer1kTokensOutput=0.06,
- speedRating=7,
- qualityRating=9,
- capabilities=[ModelCapabilitiesEnum.IMAGE_ANALYSE, ModelCapabilitiesEnum.VISION, ModelCapabilitiesEnum.MULTIMODAL],
+ speedRating=6, # Slower for vision tasks
+ qualityRating=9, # High quality vision
+ # capabilities removed (not used in business logic)
functionCall=self.callAiImage,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
- operationTypes=[OperationTypeEnum.IMAGE_ANALYSE],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.IMAGE_ANALYSE, 9)
+ ),
version="gpt-4o",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
),
AiModel(
- name="openai_generateImage",
- displayName="DALL-E 3",
+ name="dall-e-3",
+ displayName="OpenAI DALL-E 3",
connectorType="openai",
+ apiUrl="https://api.openai.com/v1/images/generations",
+ temperature=0.0, # Image generation doesn't use temperature
maxTokens=0, # Image generation doesn't use tokens
contextLength=0,
costPer1kTokensInput=0.04,
costPer1kTokensOutput=0.0,
- speedRating=6,
- qualityRating=9,
- capabilities=[ModelCapabilitiesEnum.IMAGE_GENERATE, ModelCapabilitiesEnum.ART, ModelCapabilitiesEnum.VISUAL_CREATION],
+ speedRating=5, # Slow for image generation
+ qualityRating=9, # High quality art generation
+ # capabilities removed (not used in business logic)
functionCall=self.generateImage,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
- operationTypes=[OperationTypeEnum.IMAGE_GENERATE],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.IMAGE_GENERATE, 10)
+ ),
version="dall-e-3",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04
)
@@ -143,18 +159,18 @@ class AiOpenai(BaseConnectorAi):
messages = modelCall.messages
model = modelCall.model
options = modelCall.options
- temperature = options.get("temperature", self.config.get("temperature", 0.2))
+ temperature = options.get("temperature", model.temperature)
maxTokens = model.maxTokens
payload = {
- "model": self.modelName,
+ "model": model.name,
"messages": messages,
"temperature": temperature,
"max_tokens": maxTokens
}
response = await self.httpClient.post(
- self.apiUrl,
+ model.apiUrl,
json=payload
)
@@ -184,7 +200,7 @@ class AiOpenai(BaseConnectorAi):
return AiModelResponse(
content=content,
success=True,
- modelId=self.modelName,
+ modelId=model.name,
metadata={"response_id": responseJson.get("id", "")}
)
@@ -195,19 +211,25 @@ class AiOpenai(BaseConnectorAi):
logger.error(f"Error calling OpenAI API: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error calling OpenAI API: {str(e)}")
- async def callAiImage(self, prompt: str, imageData: Union[str, bytes], mimeType: str = None) -> str:
+ async def callAiImage(self, modelCall: AiModelCall) -> AiModelResponse:
"""
- Analyzes an image with the OpenAI Vision API.
+ Analyzes an image with the OpenAI Vision API using standardized pattern.
Args:
- imageData: base64encoded data
- mimeType: The MIME type of the image (optional, only for binary data)
- prompt: The prompt for analysis
+ modelCall: AiModelCall with messages and image data in options
Returns:
- The response from the OpenAI Vision API as text
+ AiModelResponse with analysis content
"""
try:
+ # Extract parameters from modelCall
+ messages = modelCall.messages
+ model = modelCall.model
+ options = modelCall.options
+ prompt = messages[0]["content"] if messages else ""
+ imageData = options.get("imageData")
+ mimeType = options.get("mimeType", "image/jpeg")
+
logger.debug(f"Starting image analysis with query '{prompt}' for size {len(imageData)}B...")
# Ensure imageData is a string (base64 encoded)
@@ -219,10 +241,6 @@ class AiOpenai(BaseConnectorAi):
if padding_needed:
imageData += '=' * (4 - padding_needed)
- # Use default MIME type if not provided
- if not mimeType:
- mimeType = "image/jpeg"
-
logger.debug(f"Using MIME type: {mimeType}")
logger.debug(f"Base64 data length: {len(imageData)} characters")
@@ -248,18 +266,18 @@ class AiOpenai(BaseConnectorAi):
# Override the model for vision tasks
visionModel = "gpt-4o" # or "gpt-4-vision-preview" depending on availability
- # Use parameters from configuration
- temperature = self.config.get("temperature", 0.2)
+ # Use parameters from model
+ temperature = model.temperature
# Don't set maxTokens - let the model use its full context length
payload = {
- "model": visionModel,
+ "model": model.name,
"messages": messages,
"temperature": temperature
}
response = await self.httpClient.post(
- self.apiUrl,
+ model.apiUrl,
json=payload
)
@@ -269,29 +287,42 @@ class AiOpenai(BaseConnectorAi):
responseJson = response.json()
content = responseJson["choices"][0]["message"]["content"]
- return content
- # Return content
- return response
+ return AiModelResponse(
+ content=content,
+ success=True,
+ modelId=model.name,
+ metadata={"response_id": responseJson.get("id", "")}
+ )
except Exception as e:
logger.error(f"Error during image analysis: {str(e)}", exc_info=True)
- return f"[Error during image analysis: {str(e)}]"
+ return AiModelResponse(
+ content="",
+ success=False,
+ error=f"Error during image analysis: {str(e)}"
+ )
- async def generateImage(self, prompt: str, size: str = "1024x1024", quality: str = "standard", style: str = "vivid") -> Dict[str, Any]:
+ async def generateImage(self, modelCall: AiModelCall) -> AiModelResponse:
"""
- Generate an image using DALL-E 3.
+ Generate an image using DALL-E 3 using standardized pattern.
Args:
- prompt: The text prompt for image generation
- size: Image size (1024x1024, 1792x1024, or 1024x1792)
- quality: Image quality (standard or hd)
- style: Image style (vivid or natural)
+ modelCall: AiModelCall with messages and generation options
Returns:
- Dictionary with success status and image data
+ AiModelResponse with generated image data
"""
try:
+ # Extract parameters from modelCall
+ messages = modelCall.messages
+ model = modelCall.model
+ options = modelCall.options
+ prompt = messages[0]["content"] if messages else ""
+ size = options.get("size", "1024x1024")
+ quality = options.get("quality", "standard")
+ style = options.get("style", "vivid")
+
logger.debug(f"Starting image generation with prompt: '{prompt[:100]}...'")
# DALL-E 3 API endpoint
@@ -336,23 +367,29 @@ class AiOpenai(BaseConnectorAi):
image_data = responseJson["data"][0]["b64_json"]
logger.info(f"Successfully generated image: {len(image_data)} characters")
- return {
- "success": True,
- "image_data": image_data,
- "size": size,
- "quality": quality,
- "style": style
- }
+ return AiModelResponse(
+ content=image_data,
+ success=True,
+ modelId="dall-e-3",
+ metadata={
+ "size": size,
+ "quality": quality,
+ "style": style,
+ "response_id": responseJson.get("id", "")
+ }
+ )
else:
logger.error("No image data in DALL-E response")
- return {
- "success": False,
- "error": "No image data in DALL-E response"
- }
+ return AiModelResponse(
+ content="",
+ success=False,
+ error="No image data in DALL-E response"
+ )
except Exception as e:
logger.error(f"Error during image generation: {str(e)}", exc_info=True)
- return {
- "success": False,
- "error": f"Error during image generation: {str(e)}"
- }
\ No newline at end of file
+ return AiModelResponse(
+ content="",
+ success=False,
+ error=f"Error during image generation: {str(e)}"
+ )
\ No newline at end of file
diff --git a/modules/aicore/aicorePluginPerplexity.py b/modules/aicore/aicorePluginPerplexity.py
index 8ce4e9da..1ff9abc9 100644
--- a/modules/aicore/aicorePluginPerplexity.py
+++ b/modules/aicore/aicorePluginPerplexity.py
@@ -5,7 +5,7 @@ from typing import Dict, Any, List, Union, Optional
from fastapi import HTTPException
from modules.shared.configuration import APP_CONFIG
from modules.aicore.aicoreBase import BaseConnectorAi
-from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse
+from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse, createOperationTypeRatings
# Configure logger
logger = logging.getLogger(__name__)
@@ -14,9 +14,6 @@ def loadConfigData():
"""Load configuration data for Perplexity connector"""
return {
"apiKey": APP_CONFIG.get('Connector_AiPerplexity_API_SECRET'),
- "apiUrl": APP_CONFIG.get('Connector_AiPerplexity_API_URL'),
- "modelName": APP_CONFIG.get('Connector_AiPerplexity_MODEL_NAME'),
- "temperature": float(APP_CONFIG.get('Connector_AiPerplexity_TEMPERATURE')),
}
class AiPerplexity(BaseConnectorAi):
@@ -27,8 +24,6 @@ class AiPerplexity(BaseConnectorAi):
# Load configuration
self.config = loadConfigData()
self.apiKey = self.config["apiKey"]
- self.apiUrl = self.config["apiUrl"]
- self.modelName = self.config["modelName"]
# HttpClient for API calls
self.httpClient = httpx.AsyncClient(
@@ -40,7 +35,7 @@ class AiPerplexity(BaseConnectorAi):
}
)
- logger.info(f"Perplexity Connector initialized with model: {self.modelName}")
+ logger.info("Perplexity Connector initialized")
def getConnectorType(self) -> str:
"""Get the connector type identifier."""
@@ -50,92 +45,130 @@ class AiPerplexity(BaseConnectorAi):
"""Get all available Perplexity models."""
return [
AiModel(
- name="perplexity_callAiBasic",
- displayName="Llama 3.1 Sonar Large 128k",
+ name="llama-3.1-sonar-large-128k-online",
+ displayName="Perplexity Llama 3.1 Sonar Large 128k",
connectorType="perplexity",
+ apiUrl="https://api.perplexity.ai/chat/completions",
+ temperature=0.2,
maxTokens=128000,
contextLength=128000,
costPer1kTokensInput=0.005,
costPer1kTokensOutput=0.005,
speedRating=8,
qualityRating=8,
- capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.CHAT, ModelCapabilitiesEnum.REASONING, ModelCapabilitiesEnum.WEB_SEARCH],
+ # capabilities removed (not used in business logic)
functionCall=self.callAiBasic,
priority=PriorityEnum.BALANCED,
processingMode=ProcessingModeEnum.ADVANCED,
- operationTypes=[OperationTypeEnum.GENERAL, OperationTypeEnum.PLAN, OperationTypeEnum.ANALYSE, OperationTypeEnum.GENERATE, OperationTypeEnum.WEB_RESEARCH],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.PLAN, 7),
+ (OperationTypeEnum.DATA_ANALYSE, 8),
+ (OperationTypeEnum.DATA_GENERATE, 7)
+ ),
version="llama-3.1-sonar-large-128k-online",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005
),
AiModel(
- name="perplexity_callAiWithWebSearch",
- displayName="Sonar Pro",
+ name="sonar-pro",
+ displayName="Perplexity Sonar Pro",
connectorType="perplexity",
+ apiUrl="https://api.perplexity.ai/chat/completions",
+ temperature=0.2,
maxTokens=128000,
contextLength=128000,
costPer1kTokensInput=0.01,
costPer1kTokensOutput=0.01,
- speedRating=7,
- qualityRating=9,
- capabilities=[ModelCapabilitiesEnum.TEXT_GENERATION, ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.RESEARCH],
+ speedRating=6, # Slower due to AI analysis
+ qualityRating=10, # Best AI analysis quality
+ # capabilities removed (not used in business logic)
functionCall=self.callAiWithWebSearch,
priority=PriorityEnum.QUALITY,
processingMode=ProcessingModeEnum.DETAILED,
- operationTypes=[OperationTypeEnum.WEB_RESEARCH],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.WEB_RESEARCH, 10),
+ (OperationTypeEnum.WEB_SEARCH, 9),
+ (OperationTypeEnum.WEB_CRAWL, 8),
+ (OperationTypeEnum.WEB_NEWS, 8),
+ (OperationTypeEnum.WEB_QUESTIONS, 9)
+ ),
version="sonar-pro",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01
),
AiModel(
- name="perplexity_researchTopic",
- displayName="Mistral 7B Instruct",
+ name="mistral-7b-instruct",
+ displayName="Perplexity Mistral 7B Instruct",
connectorType="perplexity",
+ apiUrl="https://api.perplexity.ai/chat/completions",
+ temperature=0.2,
maxTokens=32000,
contextLength=32000,
costPer1kTokensInput=0.002,
costPer1kTokensOutput=0.002,
- speedRating=8,
- qualityRating=8,
- capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.RESEARCH, ModelCapabilitiesEnum.INFORMATION_GATHERING],
+ speedRating=9, # Fast for basic AI tasks
+ qualityRating=7, # Good but not premium quality
+ # capabilities removed (not used in business logic)
functionCall=self.researchTopic,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
- operationTypes=[OperationTypeEnum.WEB_RESEARCH],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.WEB_RESEARCH, 7),
+ (OperationTypeEnum.WEB_SEARCH, 6),
+ (OperationTypeEnum.WEB_CRAWL, 5),
+ (OperationTypeEnum.WEB_NEWS, 5),
+ (OperationTypeEnum.WEB_QUESTIONS, 6)
+ ),
version="mistral-7b-instruct",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
),
AiModel(
- name="perplexity_answerQuestion",
- displayName="Mistral 7B Instruct QA",
+ name="mistral-7b-instruct-qa",
+ displayName="Perplexity Mistral 7B Instruct QA",
connectorType="perplexity",
+ apiUrl="https://api.perplexity.ai/chat/completions",
+ temperature=0.2,
maxTokens=32000,
contextLength=32000,
costPer1kTokensInput=0.002,
costPer1kTokensOutput=0.002,
- speedRating=8,
- qualityRating=8,
- capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.QUESTION_ANSWERING, ModelCapabilitiesEnum.RESEARCH],
+ speedRating=9, # Fast for Q&A tasks
+ qualityRating=7, # Good but not premium quality
+ # capabilities removed (not used in business logic)
functionCall=self.answerQuestion,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
- operationTypes=[OperationTypeEnum.WEB_RESEARCH],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.WEB_RESEARCH, 6),
+ (OperationTypeEnum.WEB_SEARCH, 5),
+ (OperationTypeEnum.WEB_CRAWL, 4),
+ (OperationTypeEnum.WEB_NEWS, 4),
+ (OperationTypeEnum.WEB_QUESTIONS, 10)
+ ),
version="mistral-7b-instruct",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
),
AiModel(
- name="perplexity_getCurrentNews",
- displayName="Mistral 7B Instruct News",
+ name="mistral-7b-instruct-news",
+ displayName="Perplexity Mistral 7B Instruct News",
connectorType="perplexity",
+ apiUrl="https://api.perplexity.ai/chat/completions",
+ temperature=0.2,
maxTokens=32000,
contextLength=32000,
costPer1kTokensInput=0.002,
costPer1kTokensOutput=0.002,
- speedRating=8,
- qualityRating=8,
- capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.NEWS, ModelCapabilitiesEnum.CURRENT_EVENTS],
+ speedRating=9, # Fast for news tasks
+ qualityRating=7, # Good but not premium quality
+ # capabilities removed (not used in business logic)
functionCall=self.getCurrentNews,
priority=PriorityEnum.COST,
processingMode=ProcessingModeEnum.BASIC,
- operationTypes=[OperationTypeEnum.WEB_RESEARCH],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.WEB_RESEARCH, 6),
+ (OperationTypeEnum.WEB_SEARCH, 5),
+ (OperationTypeEnum.WEB_CRAWL, 4),
+ (OperationTypeEnum.WEB_NEWS, 10),
+ (OperationTypeEnum.WEB_QUESTIONS, 4)
+ ),
version="mistral-7b-instruct",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002
)
@@ -159,18 +192,18 @@ class AiPerplexity(BaseConnectorAi):
messages = modelCall.messages
model = modelCall.model
options = modelCall.options
- temperature = options.get("temperature", self.config.get("temperature", 0.2))
+ temperature = options.get("temperature", model.temperature)
maxTokens = model.maxTokens
payload = {
- "model": self.modelName,
+ "model": model.name,
"messages": messages,
"temperature": temperature,
"max_tokens": maxTokens
}
response = await self.httpClient.post(
- self.apiUrl,
+ model.apiUrl,
json=payload
)
@@ -196,7 +229,7 @@ class AiPerplexity(BaseConnectorAi):
return AiModelResponse(
content=content,
success=True,
- modelId=self.modelName,
+ modelId=model.name,
metadata={"response_id": responseJson.get("id", "")}
)
@@ -204,47 +237,33 @@ class AiPerplexity(BaseConnectorAi):
logger.error(f"Error calling Perplexity API: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error calling Perplexity API: {str(e)}")
- async def callAiWithWebSearch(self, query: str, temperature: float = None, maxTokens: int = None) -> str:
+ async def callAiWithWebSearch(self, modelCall: AiModelCall) -> AiModelResponse:
"""
- Calls Perplexity API with web search capabilities for research.
+ Calls Perplexity API with web search capabilities for research using standardized pattern.
Args:
- query: The research query or question
- temperature: Temperature for response generation (0.0-1.0)
- maxTokens: Maximum number of tokens in the response
+ modelCall: AiModelCall with messages and options
Returns:
- The response from Perplexity with web search context
+ AiModelResponse with content and metadata
"""
try:
- # Use parameters from configuration if none were overridden
- if temperature is None:
- temperature = self.config.get("temperature", 0.2)
-
- # Don't set maxTokens from config - let the model use its full context length
- # Our continuation system handles stopping early via prompt engineering
-
- # For web search, we use the configured model name
- webSearchModel = self.modelName
+ # Extract parameters from modelCall
+ messages = modelCall.messages
+ model = modelCall.model
+ options = modelCall.options
+ temperature = options.get("temperature", model.temperature)
+ maxTokens = model.maxTokens
payload = {
- "model": webSearchModel,
- "messages": [
- {
- "role": "user",
- "content": query
- }
- ],
- "temperature": temperature
+ "model": model.name,
+ "messages": messages,
+ "temperature": temperature,
+ "max_tokens": maxTokens
}
- # Add max_tokens - use provided value or throw error
- if maxTokens is None:
- raise ValueError("maxTokens must be provided for Perplexity API calls")
- payload["max_tokens"] = maxTokens
-
response = await self.httpClient.post(
- self.apiUrl,
+ model.apiUrl,
json=payload
)
@@ -265,79 +284,190 @@ class AiPerplexity(BaseConnectorAi):
responseJson = response.json()
content = responseJson["choices"][0]["message"]["content"]
- return content
+
+ return AiModelResponse(
+ content=content,
+ success=True,
+ modelId=model.name,
+ metadata={"response_id": responseJson.get("id", "")}
+ )
except Exception as e:
logger.error(f"Error calling Perplexity Web Search API: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error calling Perplexity Web Search API: {str(e)}")
- async def researchTopic(self, topic: str, depth: str = "basic") -> str:
+ async def researchTopic(self, modelCall: AiModelCall) -> AiModelResponse:
"""
- Research a topic using Perplexity's web search capabilities.
+ Research a topic using Perplexity's web search capabilities using standardized pattern.
Args:
- topic: The topic to research
- depth: Research depth - "basic", "detailed", or "comprehensive"
+ modelCall: AiModelCall with messages and options
Returns:
- Comprehensive research results on the topic
+ AiModelResponse with research content
"""
try:
- # Create research prompts based on depth
- if depth == "basic":
- prompt = f"Provide a basic overview of: {topic}"
- elif depth == "detailed":
- prompt = f"Provide a detailed analysis of: {topic}. Include recent developments, key facts, and important information."
- else: # comprehensive
- prompt = f"Provide a comprehensive research report on: {topic}. Include recent developments, key facts, statistics, expert opinions, and current trends."
+ # Extract parameters from modelCall
+ messages = modelCall.messages
+ model = modelCall.model
+ options = modelCall.options
+ temperature = options.get("temperature", model.temperature)
+ maxTokens = model.maxTokens
- return await self.callAiWithWebSearch(prompt)
+ payload = {
+ "model": model.name,
+ "messages": messages,
+ "temperature": temperature,
+ "max_tokens": maxTokens
+ }
+
+ response = await self.httpClient.post(
+ model.apiUrl,
+ json=payload
+ )
+
+ if response.status_code != 200:
+ error_detail = f"Perplexity Research API error: {response.status_code} - {response.text}"
+ logger.error(error_detail)
+
+ if response.status_code == 429:
+ error_message = "Rate limit exceeded for research. Please wait before making another request."
+ elif response.status_code == 401:
+ error_message = "Invalid API key for research. Please check your Perplexity API configuration."
+ elif response.status_code == 400:
+ error_message = f"Invalid request to Perplexity Research API: {response.text}"
+ else:
+ error_message = f"Perplexity Research API error ({response.status_code}): {response.text}"
+
+ raise HTTPException(status_code=500, detail=error_message)
+
+ responseJson = response.json()
+ content = responseJson["choices"][0]["message"]["content"]
+
+ return AiModelResponse(
+ content=content,
+ success=True,
+ modelId=model.name,
+ metadata={"response_id": responseJson.get("id", "")}
+ )
except Exception as e:
logger.error(f"Error researching topic: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error researching topic: {str(e)}")
- async def answerQuestion(self, question: str, context: str = None) -> str:
+ async def answerQuestion(self, modelCall: AiModelCall) -> AiModelResponse:
"""
- Answer a question using web search for current information.
+ Answer a question using web search for current information using standardized pattern.
Args:
- question: The question to answer
- context: Optional context to provide
+ modelCall: AiModelCall with messages and options
Returns:
- Answer with web search context
+ AiModelResponse with answer content
"""
try:
- if context:
- prompt = f"Context: {context}\n\nQuestion: {question}\n\nPlease provide a comprehensive answer using current information from the web."
- else:
- prompt = f"Question: {question}\n\nPlease provide a comprehensive answer using current information from the web."
+ # Extract parameters from modelCall
+ messages = modelCall.messages
+ model = modelCall.model
+ options = modelCall.options
+ temperature = options.get("temperature", model.temperature)
+ maxTokens = model.maxTokens
- return await self.callAiWithWebSearch(prompt)
+ payload = {
+ "model": model.name,
+ "messages": messages,
+ "temperature": temperature,
+ "max_tokens": maxTokens
+ }
+
+ response = await self.httpClient.post(
+ model.apiUrl,
+ json=payload
+ )
+
+ if response.status_code != 200:
+ error_detail = f"Perplexity Q&A API error: {response.status_code} - {response.text}"
+ logger.error(error_detail)
+
+ if response.status_code == 429:
+ error_message = "Rate limit exceeded for Q&A. Please wait before making another request."
+ elif response.status_code == 401:
+ error_message = "Invalid API key for Q&A. Please check your Perplexity API configuration."
+ elif response.status_code == 400:
+ error_message = f"Invalid request to Perplexity Q&A API: {response.text}"
+ else:
+ error_message = f"Perplexity Q&A API error ({response.status_code}): {response.text}"
+
+ raise HTTPException(status_code=500, detail=error_message)
+
+ responseJson = response.json()
+ content = responseJson["choices"][0]["message"]["content"]
+
+ return AiModelResponse(
+ content=content,
+ success=True,
+ modelId=model.name,
+ metadata={"response_id": responseJson.get("id", "")}
+ )
except Exception as e:
logger.error(f"Error answering question: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error answering question: {str(e)}")
- async def getCurrentNews(self, topic: str = None, limit: int = 5) -> str:
+ async def getCurrentNews(self, modelCall: AiModelCall) -> AiModelResponse:
"""
- Get current news on a specific topic.
+ Get current news on a specific topic using standardized pattern.
Args:
- topic: The topic to get news about (optional)
- limit: Number of news items to retrieve
+ modelCall: AiModelCall with messages and options
Returns:
- Current news information
+ AiModelResponse with news content
"""
try:
- if topic:
- prompt = f"Get the latest news about {topic}. Provide {limit} recent news items with sources and dates."
- else:
- prompt = f"Get the latest news. Provide {limit} recent news items with sources and dates."
+ # Extract parameters from modelCall
+ messages = modelCall.messages
+ model = modelCall.model
+ options = modelCall.options
+ temperature = options.get("temperature", model.temperature)
+ maxTokens = model.maxTokens
- return await self.callAiWithWebSearch(prompt)
+ payload = {
+ "model": model.name,
+ "messages": messages,
+ "temperature": temperature,
+ "max_tokens": maxTokens
+ }
+
+ response = await self.httpClient.post(
+ model.apiUrl,
+ json=payload
+ )
+
+ if response.status_code != 200:
+ error_detail = f"Perplexity News API error: {response.status_code} - {response.text}"
+ logger.error(error_detail)
+
+ if response.status_code == 429:
+ error_message = "Rate limit exceeded for news. Please wait before making another request."
+ elif response.status_code == 401:
+ error_message = "Invalid API key for news. Please check your Perplexity API configuration."
+ elif response.status_code == 400:
+ error_message = f"Invalid request to Perplexity News API: {response.text}"
+ else:
+ error_message = f"Perplexity News API error ({response.status_code}): {response.text}"
+
+ raise HTTPException(status_code=500, detail=error_message)
+
+ responseJson = response.json()
+ content = responseJson["choices"][0]["message"]["content"]
+
+ return AiModelResponse(
+ content=content,
+ success=True,
+ modelId=model.name,
+ metadata={"response_id": responseJson.get("id", "")}
+ )
except Exception as e:
logger.error(f"Error getting current news: {str(e)}")
diff --git a/modules/aicore/aicorePluginTavily.py b/modules/aicore/aicorePluginTavily.py
index b6d25f74..ee4b40ed 100644
--- a/modules/aicore/aicorePluginTavily.py
+++ b/modules/aicore/aicorePluginTavily.py
@@ -3,13 +3,14 @@
import logging
import asyncio
+import re
from dataclasses import dataclass
from typing import Optional, List
from tavily import AsyncTavilyClient
from modules.shared.configuration import APP_CONFIG
from modules.shared.timezoneUtils import get_utc_timestamp
from modules.aicore.aicoreBase import BaseConnectorAi
-from modules.datamodels.datamodelAi import AiModel, ModelCapabilitiesEnum, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelResponse
+from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelResponse, createOperationTypeRatings
logger = logging.getLogger(__name__)
@@ -69,78 +70,102 @@ class ConnectorWeb(BaseConnectorAi):
"""Get the connector type identifier."""
return "tavily"
+ def _extractUrlsFromPrompt(self, prompt: str) -> List[str]:
+ """Extract URLs from a text prompt using regex."""
+ if not prompt:
+ return []
+
+ # URL regex pattern - matches http/https URLs
+ url_pattern = r'https?://(?:[-\w.])+(?:[:\d]+)?(?:/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:#(?:[\w.])*)?)?'
+ urls = re.findall(url_pattern, prompt)
+
+ # Remove duplicates while preserving order
+ seen = set()
+ unique_urls = []
+ for url in urls:
+ if url not in seen:
+ seen.add(url)
+ unique_urls.append(url)
+
+ return unique_urls
+
def getModels(self) -> List[AiModel]:
"""Get all available Tavily models."""
return [
AiModel(
- name="tavily_search",
+ name="tavily-search",
displayName="Tavily Search",
connectorType="tavily",
+ apiUrl="https://api.tavily.com/search",
+ temperature=0.0, # Web search doesn't use temperature
maxTokens=0, # Web search doesn't use tokens
contextLength=0,
costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0,
- speedRating=8,
- qualityRating=8,
- capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.INFORMATION_RETRIEVAL, ModelCapabilitiesEnum.URL_DISCOVERY],
+ speedRating=9, # Very fast for URL discovery
+ qualityRating=9, # Excellent URL discovery quality
+ # capabilities removed (not used in business logic)
functionCall=self.search,
priority=PriorityEnum.BALANCED,
processingMode=ProcessingModeEnum.BASIC,
- operationTypes=[OperationTypeEnum.WEB_RESEARCH],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.WEB_SEARCH, 10),
+ (OperationTypeEnum.WEB_RESEARCH, 3),
+ (OperationTypeEnum.WEB_CRAWL, 2),
+ (OperationTypeEnum.WEB_NEWS, 3),
+ (OperationTypeEnum.WEB_QUESTIONS, 2)
+ ),
version="tavily-search",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived, searchDepth="basic", numRequests=1: numRequests * (1 if searchDepth == "basic" else 2) * 0.008
),
AiModel(
- name="tavily_extract",
+ name="tavily-extract",
displayName="Tavily Extract",
connectorType="tavily",
- maxTokens=0, # Web extraction doesn't use tokens
- contextLength=0,
- costPer1kTokensInput=0.0,
- costPer1kTokensOutput=0.0,
- speedRating=6,
- qualityRating=8,
- capabilities=[ModelCapabilitiesEnum.WEB_CRAWLING, ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.TEXT_EXTRACTION],
- functionCall=self.crawl,
- priority=PriorityEnum.BALANCED,
- processingMode=ProcessingModeEnum.BASIC,
- operationTypes=[OperationTypeEnum.WEB_RESEARCH],
- version="tavily-extract",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived, extractionDepth="basic", numSuccessfulUrls=1: (numSuccessfulUrls / 5) * (1 if extractionDepth == "basic" else 2) * 0.008
- ),
- AiModel(
- name="tavily_crawl",
- displayName="Tavily Crawl",
- connectorType="tavily",
+ apiUrl="https://api.tavily.com/extract",
+ temperature=0.0, # Web crawling doesn't use temperature
maxTokens=0, # Web crawling doesn't use tokens
contextLength=0,
costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0,
- speedRating=6,
- qualityRating=8,
- capabilities=[ModelCapabilitiesEnum.WEB_CRAWLING, ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.MAPPING],
+ speedRating=7, # Good for content extraction
+ qualityRating=9, # Excellent content extraction quality
+ # capabilities removed (not used in business logic)
functionCall=self.crawl,
priority=PriorityEnum.BALANCED,
processingMode=ProcessingModeEnum.BASIC,
- operationTypes=[OperationTypeEnum.WEB_RESEARCH],
- version="tavily-crawl",
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.WEB_RESEARCH, 3),
+ (OperationTypeEnum.WEB_CRAWL, 10),
+ (OperationTypeEnum.WEB_NEWS, 3),
+ (OperationTypeEnum.WEB_QUESTIONS, 2)
+ ),
+ version="tavily-extract",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived, numPages=10, extractionDepth="basic", withInstructions=False, numSuccessfulExtractions=10: ((numPages / 10) * (2 if withInstructions else 1) + (numSuccessfulExtractions / 5) * (1 if extractionDepth == "basic" else 2)) * 0.008
),
AiModel(
- name="tavily_scrape",
- displayName="Tavily Scrape",
+ name="tavily-search-extract",
+ displayName="Tavily Search & Extract",
connectorType="tavily",
+ apiUrl="https://api.tavily.com/search",
+ temperature=0.0, # Web scraping doesn't use temperature
maxTokens=0, # Web scraping doesn't use tokens
contextLength=0,
costPer1kTokensInput=0.0,
costPer1kTokensOutput=0.0,
- speedRating=6,
- qualityRating=8,
- capabilities=[ModelCapabilitiesEnum.WEB_SEARCH, ModelCapabilitiesEnum.WEB_CRAWLING, ModelCapabilitiesEnum.CONTENT_EXTRACTION, ModelCapabilitiesEnum.INFORMATION_RETRIEVAL],
+ speedRating=7, # Good for combined search+extract
+ qualityRating=8, # Good quality for structured data
+ # capabilities removed (not used in business logic)
functionCall=self.scrape,
priority=PriorityEnum.BALANCED,
processingMode=ProcessingModeEnum.BASIC,
- operationTypes=[OperationTypeEnum.WEB_RESEARCH],
+ operationTypes=createOperationTypeRatings(
+ (OperationTypeEnum.WEB_RESEARCH, 8),
+ (OperationTypeEnum.WEB_SEARCH, 6),
+ (OperationTypeEnum.WEB_CRAWL, 6),
+ (OperationTypeEnum.WEB_NEWS, 5),
+ (OperationTypeEnum.WEB_QUESTIONS, 5)
+ ),
version="tavily-search-extract",
calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived, searchDepth="basic", numSuccessfulUrls=1, extractionDepth="basic": ((1 if searchDepth == "basic" else 2) + (numSuccessfulUrls / 5) * (1 if extractionDepth == "basic" else 2)) * 0.008
)
@@ -148,9 +173,9 @@ class ConnectorWeb(BaseConnectorAi):
@classmethod
async def create(cls):
- api_key = APP_CONFIG.get("Connector_WebTavily_API_KEY_SECRET")
+ api_key = APP_CONFIG.get("Connector_AiTavily_API_SECRET")
if not api_key:
- raise ValueError("Tavily API key not configured. Please set Connector_WebTavily_API_KEY_SECRET in config.ini")
+ raise ValueError("Tavily API key not configured. Please set Connector_AiTavily_API_SECRET in config.ini")
# Load and cache web crawl related configuration
crawlTimeout = int(APP_CONFIG.get("Web_Crawl_TIMEOUT", "30"))
crawlMaxRetries = int(APP_CONFIG.get("Web_Crawl_MAX_RETRIES", "3"))
@@ -226,6 +251,18 @@ class ConnectorWeb(BaseConnectorAi):
options = modelCall.options
urls = options.get("urls", [])
+ # If no URLs provided, try to extract URLs from the prompt
+ if not urls and modelCall.messages:
+ prompt = modelCall.messages[0]["content"] if modelCall.messages else ""
+ urls = self._extractUrlsFromPrompt(prompt)
+
+ if not urls:
+ return AiModelResponse(
+ content="No URLs provided for crawling",
+ success=False,
+ error="No URLs found in options or prompt"
+ )
+
raw_results = await self._crawl(
urls,
extract_depth=options.get("extract_depth"),
diff --git a/modules/datamodels/__init__.py b/modules/datamodels/__init__.py
index 2ddc1189..e1adfd1d 100644
--- a/modules/datamodels/__init__.py
+++ b/modules/datamodels/__init__.py
@@ -3,10 +3,9 @@ Unified modules.datamodels package.
Usage examples:
from modules.datamodels import ai
- from modules.datamodels import web
+ from modules.datamodels import uam
"""
from . import datamodelAi as ai
-from . import datamodelWeb as web
from . import datamodelUam as uam
from . import datamodelSecurity as security
from . import datamodelNeutralizer as neutralizer
diff --git a/modules/datamodels/datamodelAi.py b/modules/datamodels/datamodelAi.py
index da5c1228..730c73cc 100644
--- a/modules/datamodels/datamodelAi.py
+++ b/modules/datamodels/datamodelAi.py
@@ -1,4 +1,4 @@
-from typing import Optional, List, Dict, Any, Literal, Callable, TYPE_CHECKING
+from typing import Optional, List, Dict, Any, Literal, Callable, TYPE_CHECKING, Tuple
from pydantic import BaseModel, Field
from enum import Enum
@@ -7,15 +7,54 @@ if TYPE_CHECKING:
# Operation Types
class OperationTypeEnum(str, Enum):
- GENERAL = "general"
+
+ # Planning Operation
PLAN = "plan"
- ANALYSE = "analyse"
- GENERATE = "generate"
- EXTRACT = "extract"
- WEB_RESEARCH = "webResearch"
+
+ # Data Operations
+ DATA_ANALYSE = "dataAnalyse"
+ DATA_GENERATE = "dataGenerate"
+ DATA_EXTRACT = "dataExtract"
+
+ # Image Operations
IMAGE_ANALYSE = "imageAnalyse"
IMAGE_GENERATE = "imageGenerate"
+ # Web Operations
+ WEB_SEARCH = "webSearch" # Returns list of URLs only
+ WEB_CRAWL = "webCrawl" # Returns content from given URLs
+ WEB_RESEARCH = "webResearch" # WEB_SEARCH + WEB_CRAWL combined (scrape function)
+ WEB_QUESTIONS = "webQuestions" # Question-answering web research
+ WEB_NEWS = "webNews" # News-specific web research
+
+
+# Operation Type Rating - Helper class for capability ratings
+class OperationTypeRating(BaseModel):
+ """Represents an operation type with its capability rating (1-10)."""
+ operationType: OperationTypeEnum = Field(description="The operation type")
+ rating: int = Field(ge=1, le=10, description="Capability rating (1-10, higher = better for this operation type)")
+
+ def __str__(self) -> str:
+ return f"{self.operationType.value}({self.rating})"
+
+ def __repr__(self) -> str:
+ return f"OperationTypeRating({self.operationType.value}, {self.rating})"
+
+
+# Helper function to create operation type ratings easily
+def createOperationTypeRatings(*ratings: Tuple[OperationTypeEnum, int]) -> List[OperationTypeRating]:
+ """
+ Helper function to create operation type ratings easily.
+
+ Usage:
+ operationTypes = createOperationTypeRatings(
+ (OperationTypeEnum.DATA_ANALYSE, 8),
+ (OperationTypeEnum.WEB_RESEARCH, 10),
+ (OperationTypeEnum.WEB_NEWS, 7)
+ )
+ """
+ return [OperationTypeRating(operationType=ot, rating=rating) for ot, rating in ratings]
+
# Processing Modes
class ProcessingModeEnum(str, Enum):
@@ -31,47 +70,21 @@ class PriorityEnum(str, Enum):
BALANCED = "balanced"
-# Model Capabilities Enumeration
-class ModelCapabilitiesEnum(str, Enum):
- # Text generation capabilities
- TEXT_GENERATION = "text_generation"
- CHAT = "chat"
- REASONING = "reasoning"
- ANALYSIS = "analysis"
-
- # Image capabilities
- IMAGE_ANALYSE = "imageAnalyse"
- IMAGE_GENERATE = "imageGenerate"
- VISION = "vision"
- MULTIMODAL = "multimodal"
- ART = "art"
- VISUAL_CREATION = "visual_creation"
-
- # Web capabilities
- WEB_SEARCH = "web_search"
- WEB_CRAWLING = "web_crawling"
- CONTENT_EXTRACTION = "content_extraction"
- TEXT_EXTRACTION = "text_extraction"
- INFORMATION_RETRIEVAL = "information_retrieval"
- URL_DISCOVERY = "url_discovery"
- MAPPING = "mapping"
-
- # Research capabilities
- RESEARCH = "research"
- QUESTION_ANSWERING = "question_answering"
- INFORMATION_GATHERING = "information_gathering"
- NEWS = "news"
- CURRENT_EVENTS = "current_events"
+# Model Capabilities - REMOVED: Not used in business logic
class AiModel(BaseModel):
"""Enhanced AI model definition with dynamic capabilities."""
# Core identification
- name: str = Field(description="Unique model identifier")
- displayName: str = Field(description="Human-readable model name")
+ name: str = Field(description="Actual LLM model name used for API calls")
+ displayName: str = Field(description="Human-readable model name with module prefix")
connectorType: str = Field(description="Type of connector (openai, anthropic, perplexity, tavily, etc.)")
+ # API configuration
+ apiUrl: str = Field(description="API endpoint URL for this model")
+ temperature: float = Field(default=0.2, ge=0.0, le=2.0, description="Default temperature for this model")
+
# Token and context limits
maxTokens: int = Field(description="Maximum tokens this model can generate")
contextLength: int = Field(description="Maximum context length this model can handle")
@@ -88,11 +101,10 @@ class AiModel(BaseModel):
functionCall: Optional[Callable] = Field(default=None, exclude=True, description="Function to call for this model")
calculatePriceUsd: Optional[Callable] = Field(default=None, exclude=True, description="Function to calculate price in USD")
- # Selection criteria
- capabilities: List[ModelCapabilitiesEnum] = Field(description="List of model capabilities. See ModelCapabilitiesEnum enum for available values.")
+ # Selection criteria - capabilities with ratings
priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Default priority for this model. See PriorityEnum for available values.")
processingMode: ProcessingModeEnum = Field(default=ProcessingModeEnum.BASIC, description="Default processing mode. See ProcessingModeEnum for available values.")
- operationTypes: List[OperationTypeEnum] = Field(default=[], description="Operation types this model should avoid")
+ operationTypes: List[OperationTypeRating] = Field(default=[], description="Operation types this model can handle with capability ratings (1-10)")
minContextLength: Optional[int] = Field(default=None, description="Minimum context length required")
isAvailable: bool = Field(default=True, description="Whether model is currently available")
@@ -111,7 +123,6 @@ class SelectionRule(BaseModel):
weight: float = Field(description="Weight for scoring (higher = more important)")
operationTypes: List[OperationTypeEnum] = Field(description="Operation types this rule applies to")
priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Priority level for this rule")
- capabilities: List[ModelCapabilitiesEnum] = Field(default=[], description="Required capabilities for this rule")
minQualityRating: Optional[int] = Field(default=None, description="Minimum quality rating")
maxCost: Optional[float] = Field(default=None, description="Maximum cost threshold")
minContextLength: Optional[int] = Field(default=None, description="Minimum context length required")
@@ -119,7 +130,7 @@ class SelectionRule(BaseModel):
class AiCallOptions(BaseModel):
"""Options for centralized AI processing with clear operation types and tags."""
- operationType: OperationTypeEnum = Field(default=OperationTypeEnum.GENERAL, description="Type of operation")
+ operationType: OperationTypeEnum = Field(default=OperationTypeEnum.DATA_ANALYSE, description="Type of operation")
priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Priority level")
compressPrompt: bool = Field(default=True, description="Whether to compress the prompt")
compressContext: bool = Field(default=True, description="If False: process each chunk; If True: summarize and work on summary")
@@ -131,7 +142,6 @@ class AiCallOptions(BaseModel):
resultFormat: Optional[str] = Field(default=None, description="Expected result format: txt, json, csv, xml, etc.")
safetyMargin: float = Field(default=0.1, ge=0.0, le=0.5, description="Safety margin for token limits (0.0-0.5)")
- capabilities: Optional[List[ModelCapabilitiesEnum]] = Field(default=None, description="Required model capabilities for filtering")
# Model generation parameters
temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0, description="Temperature for response generation (0.0-2.0, lower = more consistent)")
diff --git a/modules/interfaces/interfaceAiObjects.py b/modules/interfaces/interfaceAiObjects.py
index bc082ed5..a76e5e41 100644
--- a/modules/interfaces/interfaceAiObjects.py
+++ b/modules/interfaces/interfaceAiObjects.py
@@ -537,9 +537,21 @@ class AiObjects:
# Start timing
startTime = time.time()
- # Call the model's function directly
+ # Create standardized call object for image analysis
+ modelCall = AiModelCall(
+ messages=[{"role": "user", "content": prompt}],
+ model=model,
+ options={"imageData": imageData, "mimeType": mimeType}
+ )
+
+ # Call the model with standardized interface
if model.functionCall:
- content = await model.functionCall(prompt, imageData, mimeType)
+ modelResponse = await model.functionCall(modelCall)
+
+ # Extract content from standardized response
+ if not modelResponse.success:
+ raise ValueError(f"Model call failed: {modelResponse.error}")
+ content = modelResponse.content
else:
raise ValueError(f"Model {model.name} has no function call defined")
@@ -586,10 +598,21 @@ class AiObjects:
# Start timing
startTime = time.time()
- # Call the model's function directly
+ # Create standardized call object for image generation
+ modelCall = AiModelCall(
+ messages=[{"role": "user", "content": prompt}],
+ model=selectedModel,
+ options={"size": size, "quality": quality, "style": style}
+ )
+
+ # Call the model with standardized interface
if selectedModel.functionCall:
- result = await selectedModel.functionCall(prompt, size, quality, style)
- content = str(result)
+ modelResponse = await selectedModel.functionCall(modelCall)
+
+ # Extract content from standardized response
+ if not modelResponse.success:
+ raise ValueError(f"Model call failed: {modelResponse.error}")
+ content = modelResponse.content
else:
raise ValueError(f"Model {modelName} has no function call defined")
@@ -1061,10 +1084,6 @@ Format your response in a clear, professional manner that would be helpful for s
raise ValueError(f"Model {modelName} not found")
return model.dict()
- async def getModelsByCapability(self, capability: str) -> List[str]:
- """Get model names that support a specific capability."""
- models = modelRegistry.getModelsByCapability(capability)
- return [model.name for model in models]
async def getModelsByTag(self, tag: str) -> List[str]:
"""Get model names that have a specific tag."""
diff --git a/modules/services/serviceAi/subDocumentGeneration.py b/modules/services/serviceAi/subDocumentGeneration.py
index 3db28da2..86f13e2d 100644
--- a/modules/services/serviceAi/subDocumentGeneration.py
+++ b/modules/services/serviceAi/subDocumentGeneration.py
@@ -337,7 +337,7 @@ class SubDocumentGeneration:
# Prepare the AI call
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
requestOptions = AiCallOptions()
- requestOptions.operationType = OperationTypeEnum.GENERAL
+ requestOptions.operationType = OperationTypeEnum.DATA_GENERATE
# Create context with the extracted JSON content
context = f"Extracted JSON content:\n{json.dumps(docData, indent=2)}"
@@ -485,7 +485,7 @@ Return only the JSON response.
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
request_options = AiCallOptions()
- request_options.operationType = OperationTypeEnum.GENERAL
+ request_options.operationType = OperationTypeEnum.DATA_GENERATE
request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options)
response = await ai_service.aiObjects.call(request)
diff --git a/modules/services/serviceAi/subDocumentProcessing.py b/modules/services/serviceAi/subDocumentProcessing.py
index f77fa0be..726ff62d 100644
--- a/modules/services/serviceAi/subDocumentProcessing.py
+++ b/modules/services/serviceAi/subDocumentProcessing.py
@@ -71,7 +71,7 @@ class SubDocumentProcessing:
# Build extraction options WITHOUT chunking parameters
extractionOptions: Dict[str, Any] = {
"prompt": prompt,
- "operationType": options.operationType if options else "general",
+ "operationType": options.operationType if options else OperationTypeEnum.DATA_EXTRACT,
"processDocumentsIndividually": True,
# REMOVED: maxSize, textChunkSize, imageChunkSize
"mergeStrategy": {
@@ -123,7 +123,7 @@ class SubDocumentProcessing:
# Build extraction options WITHOUT chunking parameters
extractionOptions: Dict[str, Any] = {
"prompt": prompt,
- "operationType": options.operationType if options else "general",
+ "operationType": options.operationType if options else OperationTypeEnum.DATA_EXTRACT,
"processDocumentsIndividually": True,
"mergeStrategy": {
"useIntelligentMerging": True,
@@ -211,7 +211,7 @@ class SubDocumentProcessing:
# Build extraction options for chunking with intelligent merging
extractionOptions: Dict[str, Any] = {
"prompt": custom_prompt, # Use the custom prompt instead of default
- "operationType": options.operationType if options else "general",
+ "operationType": options.operationType if options else OperationTypeEnum.DATA_EXTRACT,
"processDocumentsIndividually": True, # Process each document separately
"maxSize": model_capabilities["maxContextBytes"],
"chunkAllowed": True,
@@ -766,7 +766,7 @@ CONTINUATION INSTRUCTIONS:
elif part.mimeType and part.data and len(part.data.strip()) > 0:
# Process any document container as text content
request_options = options if options is not None else AiCallOptions()
- request_options.operationType = OperationTypeEnum.GENERAL
+ request_options.operationType = OperationTypeEnum.DATA_EXTRACT
self.services.utils.debugLogToFile(f"EXTRACTION CONTAINER CHUNK {chunk_index}: Processing {part.mimeType} container as text with generate_json={generate_json}", "AI_SERVICE")
logger.info(f"Chunk {chunk_index}: Processing {part.mimeType} container as text with generate_json={generate_json}")
@@ -855,7 +855,7 @@ CONTINUATION INSTRUCTIONS:
# Ensure options is not None and set correct operation type for text
request_options = options if options is not None else AiCallOptions()
# FIXED: Set operation type to general for text processing
- request_options.operationType = OperationTypeEnum.GENERAL
+ request_options.operationType = OperationTypeEnum.DATA_EXTRACT
self.services.utils.debugLogToFile(f"EXTRACTION CHUNK {chunk_index}: Calling aiObjects.call with operationType={request_options.operationType}, generate_json={generate_json}", "AI_SERVICE")
logger.info(f"Chunk {chunk_index}: Calling aiObjects.call with operationType={request_options.operationType}, generate_json={generate_json}")
diff --git a/modules/services/serviceGeneration/renderers/rendererBaseTemplate.py b/modules/services/serviceGeneration/renderers/rendererBaseTemplate.py
index 6a91b5cf..5444525a 100644
--- a/modules/services/serviceGeneration/renderers/rendererBaseTemplate.py
+++ b/modules/services/serviceGeneration/renderers/rendererBaseTemplate.py
@@ -326,7 +326,7 @@ class BaseRenderer(ABC):
try:
request_options = AiCallOptions()
- request_options.operationType = OperationTypeEnum.GENERAL
+ request_options.operationType = OperationTypeEnum.DATA_GENERATE
request = AiCallRequest(prompt=style_template, context="", options=request_options)
diff --git a/modules/services/serviceGeneration/renderers/rendererImage.py b/modules/services/serviceGeneration/renderers/rendererImage.py
index 71f9272b..a0797bfa 100644
--- a/modules/services/serviceGeneration/renderers/rendererImage.py
+++ b/modules/services/serviceGeneration/renderers/rendererImage.py
@@ -179,7 +179,7 @@ Return only the compressed prompt, no explanations.
request = AiCallRequest(
prompt=compression_prompt,
options=AiCallOptions(
- operationType=OperationTypeEnum.GENERAL,
+ operationType=OperationTypeEnum.DATA_GENERATE,
maxTokens=None, # Let the model use its full context length
temperature=0.3 # Lower temperature for more consistent compression
)
diff --git a/modules/services/serviceGeneration/renderers/rendererPdf.py b/modules/services/serviceGeneration/renderers/rendererPdf.py
index e63e695f..f6681a79 100644
--- a/modules/services/serviceGeneration/renderers/rendererPdf.py
+++ b/modules/services/serviceGeneration/renderers/rendererPdf.py
@@ -160,7 +160,7 @@ class RendererPdf(BaseRenderer):
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
request_options = AiCallOptions()
- request_options.operationType = OperationTypeEnum.GENERAL
+ request_options.operationType = OperationTypeEnum.DATA_GENERATE
request = AiCallRequest(prompt=style_template, context="", options=request_options)
diff --git a/modules/services/serviceGeneration/renderers/rendererPptx.py b/modules/services/serviceGeneration/renderers/rendererPptx.py
index 508d2580..5c6de723 100644
--- a/modules/services/serviceGeneration/renderers/rendererPptx.py
+++ b/modules/services/serviceGeneration/renderers/rendererPptx.py
@@ -360,7 +360,7 @@ JSON ONLY. NO OTHER TEXT."""
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
request_options = AiCallOptions()
- request_options.operationType = OperationTypeEnum.GENERAL
+ request_options.operationType = OperationTypeEnum.DATA_GENERATE
request = AiCallRequest(prompt=style_template, context="", options=request_options)
diff --git a/modules/services/serviceGeneration/renderers/rendererXlsx.py b/modules/services/serviceGeneration/renderers/rendererXlsx.py
index 4e5343fb..19b36a52 100644
--- a/modules/services/serviceGeneration/renderers/rendererXlsx.py
+++ b/modules/services/serviceGeneration/renderers/rendererXlsx.py
@@ -277,7 +277,7 @@ class RendererXlsx(BaseRenderer):
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum
request_options = AiCallOptions()
- request_options.operationType = OperationTypeEnum.GENERAL
+ request_options.operationType = OperationTypeEnum.DATA_GENERATE
request = AiCallRequest(prompt=style_template, context="", options=request_options)
response = await ai_service.aiObjects.call(request)
diff --git a/modules/services/serviceGeneration/subPromptBuilder.py b/modules/services/serviceGeneration/subPromptBuilder.py
index 33c506c5..32c8ca73 100644
--- a/modules/services/serviceGeneration/subPromptBuilder.py
+++ b/modules/services/serviceGeneration/subPromptBuilder.py
@@ -381,7 +381,7 @@ Extract the main intent and requirements for document processing. Focus on:
Respond with a clear, concise statement of the extraction intent.
"""
request_options = AiCallOptions()
- request_options.operationType = OperationTypeEnum.GENERAL
+ request_options.operationType = OperationTypeEnum.DATA_GENERATE
request = AiCallRequest(prompt=analysis_prompt, context="", options=request_options)
response = await aiService.aiObjects.call(request)
diff --git a/modules/services/serviceUtils/mainServiceUtils.py b/modules/services/serviceUtils/mainServiceUtils.py
index 6d166a05..85944d28 100644
--- a/modules/services/serviceUtils/mainServiceUtils.py
+++ b/modules/services/serviceUtils/mainServiceUtils.py
@@ -207,4 +207,27 @@ class UtilsService:
return jsonUtils.parseJsonOrRaise(text)
def jsonMergeRootLists(self, parts):
- return jsonUtils.mergeRootLists(parts)
\ No newline at end of file
+ return jsonUtils.mergeRootLists(parts)
+
+ # ===== Enum utility functions =====
+
+ def mapToEnum(self, enum_class, value_str, default_value):
+ """
+ Generic function to map string value to enum, using the enum value as key.
+
+ Args:
+ enum_class: The enum class to map to
+ value_str: String value to map
+ default_value: Default enum value if no match found
+
+ Returns:
+ Matching enum value or default value
+ """
+ if not value_str:
+ return default_value
+ # Try to find enum by its value (case-insensitive)
+ for enum_item in enum_class:
+ if enum_item.value.lower() == value_str.lower():
+ return enum_item
+ # Fallback to default
+ return default_value
\ No newline at end of file
diff --git a/modules/workflows/methods/methodAi.py b/modules/workflows/methods/methodAi.py
index d2816d92..34bede22 100644
--- a/modules/workflows/methods/methodAi.py
+++ b/modules/workflows/methods/methodAi.py
@@ -10,7 +10,7 @@ from datetime import datetime, UTC
from modules.workflows.methods.methodBase import MethodBase, action
from modules.datamodels.datamodelChat import ActionResult
-from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum, ModelCapabilitiesEnum
+from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
from modules.datamodels.datamodelChat import ChatDocument
from modules.aicore.aicorePluginTavily import WebResearchRequest
@@ -34,19 +34,16 @@ class MethodAi(MethodBase):
GENERAL:
- Purpose: Process a user prompt with optional unlimited input documents to produce one or many output documents of the SAME format.
- Input requirements: aiPrompt (required); optional documentList.
- - Output format: Exactly one file format to select. For multiple output file formats to do different calls.
+ - Output format: Exactly one file format to select. For multiple output file formats you need to do different calls.
Parameters:
- aiPrompt (str, required): Instruction for the AI.
- documentList (list, optional): Document reference(s) for context.
- resultType (str, optional): Output file extension - only one extension allowed (e.g. txt, json, md, csv, xml, html, pdf, docx, xlsx, png, ...). Default: txt.
- processingMode (str, optional): basic | advanced | detailed. Default: basic.
- - includeMetadata (bool, optional): Include metadata when available. Default: True.
- - operationType (str, optional): general | plan | analyse | generate | webResearch | imageAnalyse | imageGenerate. Default: general.
- priority (str, optional): speed | quality | cost | balanced. Default: balanced.
- maxCost (float, optional): Cost limit.
- maxProcessingTime (int, optional): Time limit in seconds.
- - operationTypes (list, optional): Capability tags (e.g., text, chat, reasoning, analysis, image, vision, web, search).
"""
try:
# Init progress logger
@@ -76,54 +73,24 @@ class MethodAi(MethodBase):
documentList = [documentList]
resultType = parameters.get("resultType", "txt")
processingModeStr = parameters.get("processingMode", "basic")
- includeMetadata = parameters.get("includeMetadata", True)
- operationTypeStr = parameters.get("operationType", "general")
priorityStr = parameters.get("priority", "balanced")
maxCost = parameters.get("maxCost")
maxProcessingTime = parameters.get("maxProcessingTime")
- operationTypes = parameters.get("operationTypes")
- requiredTags = parameters.get("requiredTags", [])
- # Map string parameters to enums
- operationTypeMapping = {
- "general": OperationTypeEnum.GENERAL,
- "plan": OperationTypeEnum.PLAN,
- "analyse": OperationTypeEnum.ANALYSE,
- "generate": OperationTypeEnum.GENERATE,
- "webResearch": OperationTypeEnum.WEB_RESEARCH,
- "imageAnalyse": OperationTypeEnum.IMAGE_ANALYSE,
- "imageGenerate": OperationTypeEnum.IMAGE_GENERATE
- }
- operationType = operationTypeMapping.get(operationTypeStr, OperationTypeEnum.GENERAL)
+ # Dynamic operation type selection based on document presence
+ if documentList and len(documentList) > 0:
+ # With documents: default to dataExtract (document intelligence)
+ operationType = OperationTypeEnum.DATA_EXTRACT
+ logger.info(f"action.ai.processAuto-selected operationType EXTRACT (document intelligence mode - {len(documentList)} documents)")
+ else:
+ # Without documents: default to dataGenerate (content generation)
+ operationType = OperationTypeEnum.DATA_GENERATE
+ logger.info(f"action.ai.process Auto-selected operationType GENERATE (content generation mode - no documents)")
- priorityMapping = {
- "speed": PriorityEnum.SPEED,
- "quality": PriorityEnum.QUALITY,
- "cost": PriorityEnum.COST,
- "balanced": PriorityEnum.BALANCED
- }
- priority = priorityMapping.get(priorityStr, PriorityEnum.BALANCED)
+ # Map string parameters to enums using centralized utility function
+ priority = self.services.utils.mapToEnum(PriorityEnum, priorityStr, PriorityEnum.BALANCED)
+ processingMode = self.services.utils.mapToEnum(ProcessingModeEnum, processingModeStr, ProcessingModeEnum.BASIC)
- processingModeMapping = {
- "basic": ProcessingModeEnum.BASIC,
- "advanced": ProcessingModeEnum.ADVANCED,
- "detailed": ProcessingModeEnum.DETAILED
- }
- processingMode = processingModeMapping.get(processingModeStr, ProcessingModeEnum.BASIC)
-
- # Map requiredTags from strings to ModelCapabilitiesEnum
- if requiredTags and isinstance(requiredTags, list):
- tagMapping = {
- "text": ModelCapabilitiesEnum.TEXT_GENERATION,
- "chat": ModelCapabilitiesEnum.CHAT,
- "reasoning": ModelCapabilitiesEnum.REASONING,
- "analysis": ModelCapabilitiesEnum.ANALYSIS,
- "image": ModelCapabilitiesEnum.VISION,
- "vision": ModelCapabilitiesEnum.VISION,
- "web": ModelCapabilitiesEnum.WEB_SEARCH,
- "search": ModelCapabilitiesEnum.WEB_SEARCH
- }
- requiredTags = [tagMapping.get(tag, tag) for tag in requiredTags if isinstance(tag, str)]
if not aiPrompt:
logger.error(f"aiPrompt is missing or empty. Parameters: {parameters}")
@@ -162,7 +129,6 @@ class MethodAi(MethodBase):
resultFormat=output_format,
maxCost=maxCost,
maxProcessingTime=maxProcessingTime,
- capabilities=requiredTags if requiredTags else None
)
# Update progress - calling AI
@@ -237,9 +203,8 @@ class MethodAi(MethodBase):
- urls (list, optional): Specific URLs to crawl.
- max_results (int, optional): Max search results. Default: 5.
- max_pages (int, optional): Max pages to crawl per site. Default: 5.
- - search_depth (str, optional): basic | advanced. Default: basic.
- extract_depth (str, optional): basic | advanced. Default: advanced.
- - pages_search_depth (int, optional): Crawl depth level. Default: 2.
+ - search_depth (int, optional): Crawl depth level - how many times to follow sublinks of a page. Default: 2.
- country (str, optional): Full English country name (ISO-3166; map codes via pycountry/i18n-iso-countries).
- time_range (str, optional): d | w | m | y.
- topic (str, optional): general | news | academic.
@@ -250,9 +215,8 @@ class MethodAi(MethodBase):
urls = parameters.get("urls")
max_results = parameters.get("max_results", 5)
max_pages = parameters.get("max_pages", 5)
- search_depth = parameters.get("search_depth", "basic")
extract_depth = parameters.get("extract_depth", "advanced")
- pages_search_depth = parameters.get("pages_search_depth", 2)
+ search_depth = parameters.get("pages_search_depth", 2)
country = parameters.get("country")
time_range = parameters.get("time_range")
topic = parameters.get("topic")
@@ -316,100 +280,3 @@ class MethodAi(MethodBase):
error=str(e)
)
- def _mergeDataChunks(self, chunks: List[str], resultType: str, mimeType: str) -> str:
- """Intelligently merge data chunks using strategies based on content type"""
- try:
- if resultType == "json":
- return self._mergeJsonChunks(chunks)
- elif resultType in ["csv", "table"]:
- return self._mergeTableChunks(chunks)
- elif resultType in ["txt", "md", "text"]:
- return self._mergeTextChunks(chunks)
- else:
- # Default: simple concatenation
- return "\n".join(str(chunk) for chunk in chunks)
- except Exception as e:
- logger.warning(f"Failed to merge chunks intelligently: {str(e)}, using simple concatenation")
- return "\n".join(str(chunk) for chunk in chunks)
-
- def _mergeJsonChunks(self, chunks: List[str]) -> str:
- """Merge JSON chunks intelligently"""
- import json
-
- merged_data = []
- for i, chunk in enumerate(chunks):
- try:
- if isinstance(chunk, str):
- chunk_data = json.loads(chunk)
- else:
- chunk_data = chunk
-
- if isinstance(chunk_data, list):
- merged_data.extend(chunk_data)
- elif isinstance(chunk_data, dict):
- # For objects, merge by combining keys
- if not merged_data:
- merged_data = chunk_data
- else:
- if isinstance(merged_data, dict):
- merged_data.update(chunk_data)
- else:
- merged_data.append(chunk_data)
- else:
- merged_data.append(chunk_data)
- except Exception as e:
- logger.warning(f"Failed to parse chunk {i}: {str(e)}")
- # Add as string if JSON parsing fails
- merged_data.append(str(chunk))
-
- return json.dumps(merged_data, indent=2)
-
- def _mergeTableChunks(self, chunks: List[str]) -> str:
- """Merge table chunks (CSV) intelligently"""
- import csv
- import io
-
- merged_rows = []
- headers = None
-
- for i, chunk in enumerate(chunks):
- try:
- # Parse CSV chunk
- reader = csv.reader(io.StringIO(str(chunk)))
- rows = list(reader)
-
- if not rows:
- continue
-
- # First chunk: capture headers
- if i == 0:
- headers = rows[0] if rows else []
- merged_rows.extend(rows)
- else:
- # Subsequent chunks: skip header if it matches
- if rows and rows[0] == headers:
- merged_rows.extend(rows[1:]) # Skip duplicate header
- else:
- merged_rows.extend(rows)
-
- except Exception as e:
- logger.warning(f"Failed to parse table chunk {i}: {str(e)}")
- # Add as raw text if CSV parsing fails
- merged_rows.append([f"Raw chunk {i}: {str(chunk)[:100]}..."])
-
- # Convert back to CSV
- output = io.StringIO()
- writer = csv.writer(output)
- writer.writerows(merged_rows)
- return output.getvalue()
-
- def _mergeTextChunks(self, chunks: List[str]) -> str:
- """Merge text chunks intelligently"""
- # Simple concatenation with proper spacing
- merged = []
- for chunk in chunks:
- chunk_str = str(chunk).strip()
- if chunk_str:
- merged.append(chunk_str)
-
- return "\n\n".join(merged) # Double newline between chunks for readability
diff --git a/modules/workflows/processing/adaptive/contentValidator.py b/modules/workflows/processing/adaptive/contentValidator.py
index 1f6a0aed..1b28e752 100644
--- a/modules/workflows/processing/adaptive/contentValidator.py
+++ b/modules/workflows/processing/adaptive/contentValidator.py
@@ -118,7 +118,7 @@ DELIVERED CONTENT TO CHECK:
# Call AI service for validation
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
request_options = AiCallOptions()
- request_options.operationType = OperationTypeEnum.GENERAL
+ request_options.operationType = OperationTypeEnum.DATA_ANALYSE
response = await self.services.ai.callAiPlanning(
prompt=validationPrompt,
diff --git a/modules/workflows/processing/adaptive/intentAnalyzer.py b/modules/workflows/processing/adaptive/intentAnalyzer.py
index 4bc7aa55..ba9629b1 100644
--- a/modules/workflows/processing/adaptive/intentAnalyzer.py
+++ b/modules/workflows/processing/adaptive/intentAnalyzer.py
@@ -61,7 +61,7 @@ CRITICAL: Respond with ONLY the JSON object below. Do not include any explanator
# Call AI service for analysis
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum
request_options = AiCallOptions()
- request_options.operationType = OperationTypeEnum.GENERAL
+ request_options.operationType = OperationTypeEnum.DATA_ANALYSE
response = await self.services.ai.callAiPlanning(
prompt=analysisPrompt,
diff --git a/modules/workflows/processing/modes/modeActionplan.py b/modules/workflows/processing/modes/modeActionplan.py
index aaf25254..632ae138 100644
--- a/modules/workflows/processing/modes/modeActionplan.py
+++ b/modules/workflows/processing/modes/modeActionplan.py
@@ -457,7 +457,7 @@ class ActionplanMode(BaseMode):
# Centralized AI call: Result validation (balanced analysis) with placeholders
options = AiCallOptions(
- operationType=OperationTypeEnum.ANALYSE,
+ operationType=OperationTypeEnum.DATA_ANALYSE,
priority=PriorityEnum.BALANCED,
compressPrompt=True,
compressContext=False,
diff --git a/modules/workflows/processing/modes/modeReact.py b/modules/workflows/processing/modes/modeReact.py
index de2b0db9..1acd8152 100644
--- a/modules/workflows/processing/modes/modeReact.py
+++ b/modules/workflows/processing/modes/modeReact.py
@@ -296,7 +296,7 @@ class ReactMode(BaseMode):
# Centralized AI call for parameter suggestion (balanced analysis)
options = AiCallOptions(
- operationType=OperationTypeEnum.ANALYSE,
+ operationType=OperationTypeEnum.DATA_ANALYSE,
priority=PriorityEnum.BALANCED,
compressPrompt=True,
compressContext=False,
@@ -611,7 +611,7 @@ class ReactMode(BaseMode):
# Centralized AI call for refinement decision (balanced analysis)
options = AiCallOptions(
- operationType=OperationTypeEnum.ANALYSE,
+ operationType=OperationTypeEnum.DATA_ANALYSE,
priority=PriorityEnum.BALANCED,
compressPrompt=True,
compressContext=False,
@@ -718,7 +718,7 @@ Return only the user-friendly message, no technical details."""
prompt=prompt,
placeholders=None,
options=AiCallOptions(
- operationType=OperationTypeEnum.GENERATE,
+ operationType=OperationTypeEnum.DATA_GENERATE,
priority=PriorityEnum.SPEED,
compressPrompt=True,
maxCost=0.01,
@@ -759,7 +759,7 @@ Return only the user-friendly message, no technical details."""
prompt=prompt,
placeholders=None,
options=AiCallOptions(
- operationType=OperationTypeEnum.GENERATE,
+ operationType=OperationTypeEnum.DATA_GENERATE,
priority=PriorityEnum.SPEED,
compressPrompt=True,
maxCost=0.01,
diff --git a/test_ai_model_selection.py b/test_ai_model_selection.py
index 2f7b016f..ea8a6798 100644
--- a/test_ai_model_selection.py
+++ b/test_ai_model_selection.py
@@ -1,16 +1,16 @@
#!/usr/bin/env python3
"""
-AI Model Selection Test - Prints prioritized fallback model lists used for AI calls
+AI Model Selection Test - Prints prioritized fallback model lists for all interface calls
-Scenarios mirror typical calls in workflows/ (task planning, action planning,
-analysis, and react-mode decisions), showing which models are shortlisted and
-their final prioritized order after rating and cost tie-breaking.
+Tests all main interface methods in interfaceAiObjects.py and shows which models
+are selected for each type of AI operation (text generation, image analysis,
+image generation, web research, etc.).
"""
import asyncio
import os
import sys
-from typing import List, Tuple
+import base64
# Ensure gateway is on path when running directly
@@ -19,6 +19,8 @@ sys.path.append(os.path.dirname(__file__))
from modules.features.chatPlayground.mainChatPlayground import getServices
from modules.datamodels.datamodelAi import (
AiCallOptions,
+ AiCallRequest,
+ AiModelCall,
OperationTypeEnum,
PriorityEnum,
ProcessingModeEnum,
@@ -42,8 +44,10 @@ class ModelSelectionTester:
async def initialize(self) -> None:
from modules.services.serviceAi.mainServiceAi import AiService
+ from modules.interfaces.interfaceAiObjects import AiObjects
self.services.ai = await AiService.create(self.services)
+ self.aiObjects = await AiObjects.create()
async def _printFallbackListWithContext(self, title: str, prompt: str, context: str, options: AiCallOptions) -> None:
print(f"\n{'='*80}")
@@ -137,166 +141,355 @@ class ModelSelectionTester:
print(f" Size: {sizeRating:.3f}, ProcessingMode: {processingModeRating:.3f}, Priority: {priorityRating:.3f}")
async def run(self) -> None:
- # Scenarios reflecting workflows/
- scenarios: List[Tuple[str, str, AiCallOptions]] = []
+ """Test model selection for all interface methods."""
+ print("=" * 100)
+ print("AI INTERFACE MODEL SELECTION TEST")
+ print("=" * 100)
+ print("Testing model selection for all interface methods in interfaceAiObjects.py")
+ print("=" * 100)
- # Task planning (taskPlanner, modeActionplan)
- scenarios.append(
- (
- "PLAN - Quality, Detailed",
- "Task planning for a multi-step business workflow.",
- AiCallOptions(
- operationType=OperationTypeEnum.PLAN,
- priority=PriorityEnum.QUALITY,
- compressPrompt=False,
- compressContext=False,
- processingMode=ProcessingModeEnum.DETAILED,
- maxCost=0.10,
- maxProcessingTime=30,
- ),
- )
- )
+ # Test 1: Text Generation (call method)
+ await self._testTextGeneration()
- # Result validation / analysis (modeActionplan)
- scenarios.append(
- (
- "ANALYSE - Balanced, Advanced",
- "Validate action plan correctness and completeness.",
- AiCallOptions(
- operationType=OperationTypeEnum.ANALYSE,
- priority=PriorityEnum.BALANCED,
- compressPrompt=True,
- compressContext=False,
- processingMode=ProcessingModeEnum.ADVANCED,
- maxCost=0.05,
- maxProcessingTime=30,
- ),
- )
- )
+ # Test 2: Image Analysis (callImage method)
+ await self._testImageAnalysis()
- # React mode - action selection (modeReact)
- scenarios.append(
- (
- "GENERAL - Balanced, Advanced (React: action selection)",
- "Select next best action from context and state.",
- AiCallOptions(
- operationType=OperationTypeEnum.GENERAL,
- priority=PriorityEnum.BALANCED,
- compressPrompt=True,
- compressContext=True,
- processingMode=ProcessingModeEnum.ADVANCED,
- maxCost=0.03,
- maxProcessingTime=20,
- ),
- )
- )
+ # Test 3: Image Generation (generateImage method)
+ await self._testImageGeneration()
- # React mode - parameter suggestion (modeReact example)
- scenarios.append(
- (
- "ANALYSE - Balanced, Advanced (React: parameter suggestion)",
- "Suggest parameters for the selected action as JSON.",
- AiCallOptions(
- operationType=OperationTypeEnum.ANALYSE,
- priority=PriorityEnum.BALANCED,
- compressPrompt=True,
- compressContext=False,
- processingMode=ProcessingModeEnum.ADVANCED,
- maxCost=0.05,
- maxProcessingTime=30,
- resultFormat="json",
- temperature=0.3,
- ),
- )
- )
+ # Test 4: Web Search (searchWebsites method)
+ await self._testWebSearch()
- # Intent analysis (user input understanding)
- scenarios.append(
- (
- "ANALYSE - Quality, Detailed (Intent Analysis)",
- "Analyze user intent and extract key requirements from the following request: 'I need to create a comprehensive marketing strategy for our new product launch including budget allocation, timeline, and target audience analysis.'",
- AiCallOptions(
- operationType=OperationTypeEnum.ANALYSE,
- priority=PriorityEnum.QUALITY,
- compressPrompt=False,
- compressContext=False,
- processingMode=ProcessingModeEnum.DETAILED,
- maxCost=0.08,
- maxProcessingTime=45,
- resultFormat="json",
- temperature=0.2,
- ),
- )
- )
+ # Test 5: Web Crawling (crawlWebsites method)
+ await self._testWebCrawling()
- # Review/Validation (quality assurance)
- scenarios.append(
- (
- "ANALYSE - Quality, Detailed (Review/Validation)",
- "Review and validate the following business proposal for completeness, accuracy, and compliance with industry standards. Identify any gaps or areas for improvement.",
- AiCallOptions(
- operationType=OperationTypeEnum.ANALYSE,
- priority=PriorityEnum.QUALITY,
- compressPrompt=False,
- compressContext=False,
- processingMode=ProcessingModeEnum.DETAILED,
- maxCost=0.10,
- maxProcessingTime=60,
- resultFormat="json",
- temperature=0.1,
- ),
- )
- )
+ # Test 6: Web Research (webQuery method)
+ await self._testWebResearch()
- # Large context scenario (to test size-based scoring)
- scenarios.append(
- (
- "GENERAL - Balanced, Advanced (Large Context Test)",
- "Process this large document and provide a comprehensive summary.",
- AiCallOptions(
- operationType=OperationTypeEnum.GENERAL,
- priority=PriorityEnum.BALANCED,
- compressPrompt=False,
- compressContext=False,
- processingMode=ProcessingModeEnum.ADVANCED,
- maxCost=0.15,
- maxProcessingTime=120,
- ),
- )
- )
+ # Test 7: Content Analysis with Chunking
+ await self._testContentAnalysis()
- # Iterate and print lists
- for title, prompt, options in scenarios:
- await self._printFallbackList(title, prompt, options)
+ # Test 8: Website Selection
+ await self._testWebsiteSelection()
- # Test with actual context to see size-based scoring
- largeContext = """
- This is a comprehensive business document containing detailed information about our company's strategic initiatives,
- financial performance, market analysis, competitive landscape, operational metrics, customer feedback,
- product development roadmap, technology stack, human resources, legal compliance, risk management,
- sustainability efforts, and future growth plans. The document spans multiple sections including executive summary,
- market research, financial statements, operational reports, customer insights, product specifications,
- technology architecture, HR policies, legal frameworks, risk assessments, environmental impact studies,
- and strategic recommendations. This extensive content is designed to test the model selection algorithm's
- ability to handle large context sizes and make intelligent decisions about which models are best suited
- for processing such substantial amounts of information while maintaining efficiency and cost-effectiveness.
- """ * 10 # Repeat to make it even larger
+ # Test 9: Actual Interface Calls
+ await self._testActualInterfaceCalls()
- await self._printFallbackListWithContext(
- "GENERAL - Balanced, Advanced (Large Context Test)",
- "Analyze this comprehensive business document and provide key insights.",
- largeContext,
- AiCallOptions(
- operationType=OperationTypeEnum.GENERAL,
+ # Show model registry summary
+ await self._showModelSummary()
+
+ print("\n" + "=" * 100)
+ print("ALL INTERFACE TESTS COMPLETED")
+ print("=" * 100)
+
+ async def _testTextGeneration(self) -> None:
+ """Test model selection for text generation calls."""
+ print(f"\n{'='*80}")
+ print("1. TEXT GENERATION (call method)")
+ print(f"{'='*80}")
+
+ # Test different text generation scenarios
+ scenarios = [
+ ("Text Analysis", "Write a summary about artificial intelligence trends.", OperationTypeEnum.DATA_ANALYSE),
+ ("Planning Task", "Create a project plan for software development.", OperationTypeEnum.PLAN),
+ ("Analysis Task", "Analyze the pros and cons of cloud computing.", OperationTypeEnum.DATA_ANALYSE),
+ ]
+
+ for title, prompt, operation_type in scenarios:
+ options = AiCallOptions(
+ operationType=operation_type,
priority=PriorityEnum.BALANCED,
- compressPrompt=False,
- compressContext=False,
processingMode=ProcessingModeEnum.ADVANCED,
- maxCost=0.15,
- maxProcessingTime=120,
- ),
+ maxCost=0.05,
+ maxProcessingTime=30,
+ )
+ await self._printFallbackList(f" {title}", prompt, options)
+
+ async def _testImageAnalysis(self) -> None:
+ """Test model selection for image analysis calls."""
+ print(f"\n{'='*80}")
+ print("2. IMAGE ANALYSIS (callImage method)")
+ print(f"{'='*80}")
+
+ # Create a small test image (1x1 pixel PNG)
+ test_image_data = base64.b64encode(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\nIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xdd\x8d\xb4\x1c\x00\x00\x00\x00IEND\xaeB`\x82').decode('utf-8')
+
+ options = AiCallOptions(
+ operationType=OperationTypeEnum.IMAGE_ANALYSE,
+ priority=PriorityEnum.BALANCED,
+ processingMode=ProcessingModeEnum.ADVANCED,
+ maxCost=0.02,
+ maxProcessingTime=20,
)
+ prompt = "Describe what you see in this image."
+ await self._printFallbackList(" Image Analysis", prompt, options)
+
+ async def _testImageGeneration(self) -> None:
+ """Test model selection for image generation calls."""
+ print(f"\n{'='*80}")
+ print("3. IMAGE GENERATION (generateImage method)")
+ print(f"{'='*80}")
+
+ options = AiCallOptions(
+ operationType=OperationTypeEnum.IMAGE_GENERATE,
+ priority=PriorityEnum.QUALITY,
+ processingMode=ProcessingModeEnum.DETAILED,
+ maxCost=0.10,
+ maxProcessingTime=60,
+ )
+
+ prompt = "A futuristic cityscape with flying cars and neon lights."
+ await self._printFallbackList(" Image Generation", prompt, options)
+
+ async def _testWebResearch(self) -> None:
+ """Test model selection for web research calls."""
+ print(f"\n{'='*80}")
+ print("6. WEB RESEARCH (webQuery method)")
+ print(f"{'='*80}")
+
+ options = AiCallOptions(
+ operationType=OperationTypeEnum.WEB_RESEARCH,
+ priority=PriorityEnum.BALANCED,
+ processingMode=ProcessingModeEnum.ADVANCED,
+ maxCost=0.05,
+ maxProcessingTime=30,
+ )
+
+ prompt = "What are the latest trends in artificial intelligence?"
+ await self._printFallbackList(" Web Research", prompt, options)
+
+ async def _testWebSearch(self) -> None:
+ """Test model selection for web search calls."""
+ print(f"\n{'='*80}")
+ print("4. WEB SEARCH (searchWebsites method)")
+ print(f"{'='*80}")
+
+ options = AiCallOptions(
+ operationType=OperationTypeEnum.WEB_SEARCH,
+ priority=PriorityEnum.BALANCED,
+ processingMode=ProcessingModeEnum.BASIC,
+ maxCost=0.01,
+ maxProcessingTime=30,
+ )
+
+ prompt = "Search for artificial intelligence companies"
+ await self._printFallbackList(" Web Search", prompt, options)
+
+ async def _testWebCrawling(self) -> None:
+ """Test model selection for web crawling calls."""
+ print(f"\n{'='*80}")
+ print("5. WEB CRAWLING (crawlWebsites method)")
+ print(f"{'='*80}")
+
+ options = AiCallOptions(
+ operationType=OperationTypeEnum.WEB_CRAWL,
+ priority=PriorityEnum.BALANCED,
+ processingMode=ProcessingModeEnum.BASIC,
+ maxCost=0.02,
+ maxProcessingTime=60,
+ )
+
+ prompt = "Crawl content from these URLs"
+ await self._printFallbackList(" Web Crawling", prompt, options)
+
+ async def _testContentAnalysis(self) -> None:
+ """Test model selection for content analysis with chunking."""
+ print(f"\n{'='*80}")
+ print("7. CONTENT ANALYSIS WITH CHUNKING")
+ print(f"{'='*80}")
+
+ # Test with large content to trigger chunking
+ large_content = {
+ "https://example.com/page1": "This is a large document about artificial intelligence. " * 1000,
+ "https://example.com/page2": "This is another large document about machine learning. " * 1000,
+ }
+
+ options = AiCallOptions(
+ operationType=OperationTypeEnum.DATA_ANALYSE,
+ priority=PriorityEnum.BALANCED,
+ processingMode=ProcessingModeEnum.ADVANCED,
+ maxCost=0.10,
+ maxProcessingTime=60,
+ )
+
+ prompt = "Analyze this content and provide key insights."
+ await self._printFallbackList(" Content Analysis", prompt, options)
+
+ async def _testWebsiteSelection(self) -> None:
+ """Test model selection for website selection."""
+ print(f"\n{'='*80}")
+ print("8. WEBSITE SELECTION (selectRelevantWebsites method)")
+ print(f"{'='*80}")
+
+ # This method uses webQuery internally, so it uses the same model selection as web research
+ options = AiCallOptions(
+ operationType=OperationTypeEnum.WEB_RESEARCH,
+ priority=PriorityEnum.BALANCED,
+ processingMode=ProcessingModeEnum.ADVANCED,
+ maxCost=0.03,
+ maxProcessingTime=20,
+ )
+
+ prompt = "Select the most relevant websites from this list for AI research."
+ await self._printFallbackList(" Website Selection", prompt, options)
+
+ async def _testActualInterfaceCalls(self) -> None:
+ """Test actual interface calls to show real model selection."""
+ print(f"\n{'='*80}")
+ print("9. ACTUAL INTERFACE CALLS (Real Model Selection)")
+ print(f"{'='*80}")
+
+ # Test 1: Text generation call
+ print("\n Testing: aiObjects.call() - Text Generation")
+ try:
+ request = AiCallRequest(
+ prompt="Write a short summary about machine learning.",
+ context="",
+ options=AiCallOptions(
+ operationType=OperationTypeEnum.DATA_ANALYSE,
+ priority=PriorityEnum.BALANCED,
+ processingMode=ProcessingModeEnum.ADVANCED,
+ maxCost=0.05,
+ maxProcessingTime=30,
+ )
+ )
+
+ # Get the model selection that would be used
+ availableModels = modelRegistry.getAvailableModels()
+ failoverModelList = modelSelector.getFailoverModelList(
+ prompt=request.prompt,
+ context=request.context,
+ options=request.options,
+ availableModels=availableModels,
+ )
+
+ if failoverModelList:
+ print(f" Selected model: {failoverModelList[0].name}")
+ print(f" Fallback models: {[m.name for m in failoverModelList[1:3]]}")
+ else:
+ print(" No suitable models found")
+
+ except Exception as e:
+ print(f" Error: {e}")
+
+ # Test 2: Image analysis call
+ print("\n Testing: aiObjects.callImage() - Image Analysis")
+ try:
+ options = AiCallOptions(
+ operationType=OperationTypeEnum.IMAGE_ANALYSE,
+ priority=PriorityEnum.BALANCED,
+ processingMode=ProcessingModeEnum.ADVANCED,
+ maxCost=0.02,
+ maxProcessingTime=20,
+ )
+
+ availableModels = modelRegistry.getAvailableModels()
+ failoverModelList = modelSelector.getFailoverModelList(
+ prompt="Describe this image",
+ context="",
+ options=options,
+ availableModels=availableModels,
+ )
+
+ if failoverModelList:
+ print(f" Selected model: {failoverModelList[0].name}")
+ print(f" Fallback models: {[m.name for m in failoverModelList[1:3]]}")
+ else:
+ print(" No suitable models found")
+
+ except Exception as e:
+ print(f" Error: {e}")
+
+ # Test 3: Image generation call
+ print("\n Testing: aiObjects.generateImage() - Image Generation")
+ try:
+ options = AiCallOptions(
+ operationType=OperationTypeEnum.IMAGE_GENERATE,
+ priority=PriorityEnum.QUALITY,
+ processingMode=ProcessingModeEnum.DETAILED,
+ maxCost=0.10,
+ maxProcessingTime=60,
+ )
+
+ availableModels = modelRegistry.getAvailableModels()
+ failoverModelList = modelSelector.getFailoverModelList(
+ prompt="A futuristic cityscape",
+ context="",
+ options=options,
+ availableModels=availableModels,
+ )
+
+ if failoverModelList:
+ print(f" Selected model: {failoverModelList[0].name}")
+ print(f" Fallback models: {[m.name for m in failoverModelList[1:3]]}")
+ else:
+ print(" No suitable models found")
+
+ except Exception as e:
+ print(f" Error: {e}")
+
+ # Test 4: Web research call
+ print("\n Testing: aiObjects.webQuery() - Web Research")
+ try:
+ options = AiCallOptions(
+ operationType=OperationTypeEnum.WEB_RESEARCH,
+ priority=PriorityEnum.BALANCED,
+ processingMode=ProcessingModeEnum.ADVANCED,
+ maxCost=0.05,
+ maxProcessingTime=30,
+ )
+
+ availableModels = modelRegistry.getAvailableModels()
+ failoverModelList = modelSelector.getFailoverModelList(
+ prompt="What are AI trends?",
+ context="",
+ options=options,
+ availableModels=availableModels,
+ )
+
+ if failoverModelList:
+ print(f" Selected model: {failoverModelList[0].name}")
+ print(f" Fallback models: {[m.name for m in failoverModelList[1:3]]}")
+ else:
+ print(" No suitable models found")
+
+ except Exception as e:
+ print(f" Error: {e}")
+
+ async def _showModelSummary(self) -> None:
+ """Show summary of all available models and their capabilities."""
+ print(f"\n{'='*80}")
+ print("MODEL REGISTRY SUMMARY")
+ print(f"{'='*80}")
+
+ availableModels = modelRegistry.getAvailableModels()
+ print(f"Total models available: {len(availableModels)}")
+
+ # Group by connector type
+ by_connector = {}
+ for model in availableModels:
+ connector_type = getattr(model, 'connectorType', 'unknown')
+ if connector_type not in by_connector:
+ by_connector[connector_type] = []
+ by_connector[connector_type].append(model)
+
+ print(f"\nModels by connector type:")
+ for connector_type, models in by_connector.items():
+ print(f" {connector_type}: {len(models)} models")
+ for model in models:
+ capabilities = getattr(model, 'capabilities', [])
+ print(f" - {model.name}: {capabilities}")
+
+ # Show operation type support
+ print(f"\nOperation type support:")
+ for op_type in OperationTypeEnum:
+ supported_models = [m for m in availableModels if hasattr(m, 'operationTypes') and op_type in m.operationTypes]
+ print(f" {op_type.name}: {len(supported_models)} models")
+ if supported_models:
+ model_names = [m.name for m in supported_models[:3]] # Show first 3 models
+ print(f" Models: {', '.join(model_names)}")
+
async def main() -> None:
tester = ModelSelectionTester()
diff --git a/test_operation_type_ratings.py b/test_operation_type_ratings.py
new file mode 100644
index 00000000..907f9fd8
--- /dev/null
+++ b/test_operation_type_ratings.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+"""
+Test script to demonstrate the new operation type rating system.
+This shows how models are now sorted by their capability ratings for specific operation types.
+"""
+
+import sys
+import os
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+
+from modules.datamodels.datamodelAi import OperationTypeEnum, createOperationTypeRatings, AiCallOptions, PriorityEnum, ProcessingModeEnum
+from modules.aicore.aicorePluginPerplexity import AiPerplexity
+from modules.aicore.aicorePluginTavily import AiTavily
+from modules.aicore.aicoreModelSelector import ModelSelector
+
+def testOperationTypeRatings():
+ """Test the new operation type rating system."""
+ print("๐งช Testing Operation Type Rating System")
+ print("=" * 50)
+
+ # Initialize connectors
+ perplexity = AiPerplexity()
+ tavily = AiTavily()
+ modelSelector = ModelSelector()
+
+ # Get all models
+ allModels = perplexity.getModels() + tavily.getModels()
+
+ print(f"๐ Total models available: {len(allModels)}")
+ print()
+
+ # Test different operation types
+ testCases = [
+ (OperationTypeEnum.WEB_RESEARCH, "Web Research"),
+ (OperationTypeEnum.WEB_NEWS, "Web News"),
+ (OperationTypeEnum.WEB_QUESTIONS, "Web Questions"),
+ (OperationTypeEnum.WEB_SEARCH, "Web Search"),
+ (OperationTypeEnum.DATA_ANALYSE, "Text Analysis tasks")
+ ]
+
+ for operationType, description in testCases:
+ print(f"๐ฏ Testing: {description} ({operationType.value})")
+ print("-" * 40)
+
+ # Create AI call options
+ options = AiCallOptions(
+ operationType=operationType,
+ priority=PriorityEnum.BALANCED,
+ processingMode=ProcessingModeEnum.BASIC
+ )
+
+ # Get failover model list (sorted by rating)
+ failoverModels = modelSelector.getFailoverModelList(
+ prompt="Test prompt",
+ context="Test context",
+ options=options,
+ availableModels=allModels
+ )
+
+ if failoverModels:
+ print(f"โ
Found {len(failoverModels)} suitable models:")
+ for i, model in enumerate(failoverModels[:5]): # Show top 5
+ # Get the rating for this operation type
+ rating = 0
+ for ot_rating in model.operationTypes:
+ if ot_rating.operationType == operationType:
+ rating = ot_rating.rating
+ break
+
+ print(f" {i+1}. {model.displayName}")
+ print(f" Rating: {rating}/10 | Speed: {model.speedRating}/10 | Quality: {model.qualityRating}/10")
+ print(f" Cost: ${model.costPer1kTokensInput:.4f}/1k tokens")
+ else:
+ print("โ No suitable models found")
+
+ print()
+
+ # Test the helper function
+ print("๐ง Testing Helper Function")
+ print("-" * 30)
+
+ # Create operation type ratings using the helper
+ ratings = createOperationTypeRatings(
+ (OperationTypeEnum.WEB_RESEARCH, 10),
+ (OperationTypeEnum.WEB_NEWS, 8),
+ (OperationTypeEnum.DATA_ANALYSE, 6)
+ )
+
+ print("Created ratings:")
+ for rating in ratings:
+ print(f" {rating.operationType.value}: {rating.rating}/10")
+
+ print()
+ print("โ
All tests completed successfully!")
+
+if __name__ == "__main__":
+ testOperationTypeRatings()