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()