@@ -268,9 +268,104 @@ def generatewithopenaicompatible(self, messages: list[dict[str, str]]) -> str:
268268 return self .validatereply (reply )
269269
270270
271+ '''EcyltFreeGPTIssueReplyBot'''
272+ class EcyltFreeGPTIssueReplyBot (G4FIssueReplyBot ):
273+ def __init__ (self ) -> None :
274+ super (EcyltFreeGPTIssueReplyBot , self ).__init__ ()
275+ self .ecylt_enabled = EcyltFreeGPTIssueReplyBot .getenv ("ECYLT_FREE_GPT_ENABLED" , "true" ).lower ()
276+ self .ecylt_api_url = EcyltFreeGPTIssueReplyBot .getenv ("ECYLT_FREE_GPT_URL" , "https://api.ecylt.top/v1/free_gpt/chat_json.php" )
277+ '''generatereply'''
278+ def generatereply (self , issue : dict [str , Any ], repository_context : str ) -> str :
279+ if not self .ecyltenabled (): print ("Ecylt Free GPT API is disabled. Falling back to g4f." ); return super ().generatereply (issue , repository_context )
280+ messages = self .buildmessages (issue , repository_context )
281+ try :
282+ reply = self .generatewithecylt (messages ); print ("Ecylt Free GPT API succeeded." ); return reply
283+ except Exception as error :
284+ print ("Ecylt Free GPT API failed. Falling back to g4f." )
285+ print (repr (error )); traceback .print_exc ()
286+ return super ().generatereply (issue , repository_context )
287+ '''ecyltenabled'''
288+ def ecyltenabled (self ) -> bool :
289+ return self .ecylt_enabled not in {"0" , "false" , "no" , "off" }
290+ '''generatewithecylt'''
291+ def generatewithecylt (self , messages : list [dict [str , str ]]) -> str :
292+ conversation_id = None ; system_prompt , user_prompt = self .splitmessages (messages )
293+ try :
294+ conversation_id = self .extractconversationid (self .ecyltpost ({"action" : "new" , "system_prompt" : system_prompt }))
295+ reply = self .extractreplytext (self .ecyltpost ({"action" : "continue" , "message" : user_prompt , "conversation_id" : conversation_id }))
296+ return self .validatereply (reply )
297+ finally :
298+ if conversation_id : self .deleteecyltconversation (conversation_id )
299+ '''splitmessages'''
300+ @staticmethod
301+ def splitmessages (messages : list [dict [str , str ]]) -> tuple [str , str ]:
302+ system_parts , user_parts = [], []
303+ for message in messages :
304+ role , content = message .get ("role" ), message .get ("content" , "" )
305+ if role == "system" : system_parts .append (content )
306+ else : user_parts .append (content )
307+ return "\n \n " .join (system_parts ), "\n \n " .join (user_parts )
308+ '''ecyltpost'''
309+ def ecyltpost (self , payload : dict [str , Any ]) -> dict [str , Any ]:
310+ resp = requests .post (self .ecylt_api_url , json = payload , timeout = self .timeout_seconds )
311+ if resp .status_code >= 300 : raise RuntimeError (f"Ecylt API failed: { resp .status_code } \n { resp .text } " )
312+ try : data = resp .json ()
313+ except Exception as error : raise RuntimeError (f"Ecylt API returned non-JSON response: { resp .text [:500 ]} " ) from error
314+ if isinstance (data , dict ): return data
315+ raise RuntimeError (f"Ecylt API returned unexpected response: { data !r} " )
316+ '''extractconversationid'''
317+ @staticmethod
318+ def extractconversationid (data : dict [str , Any ]) -> str :
319+ candidate_keys = ["conversation_id" , "conversationId" , "id" , "data" ]
320+ for key in candidate_keys :
321+ if isinstance ((value := data .get (key )), str ) and value .strip (): return value .strip ()
322+ if isinstance (value , dict ) and isinstance (nested_value := (value .get ("conversation_id" ) or value .get ("id" )), str ) and nested_value .strip (): return nested_value .strip ()
323+ raise RuntimeError (f"Conversation id not found in response: { data !r} " )
324+ '''extractreplytext'''
325+ @staticmethod
326+ def extractreplytext (data : dict [str , Any ]) -> str :
327+ candidate_keys = ["reply" , "message" , "content" , "answer" , "text" , "response" , "result" , "data" ]
328+ for key in candidate_keys :
329+ if isinstance ((value := data .get (key )), str ) and value .strip (): return value .strip ()
330+ if isinstance (value , dict ) and (nested_text := EcyltFreeGPTIssueReplyBot .extractreplytext (value )): return nested_text
331+ raise RuntimeError (f"Reply text not found in response: { data !r} " )
332+ '''deleteecyltconversation'''
333+ def deleteecyltconversation (self , conversation_id : str ) -> None :
334+ try : self .ecyltpost ({"action" : "delete" , "conversation_id" : conversation_id }); print ("Ecylt conversation deleted." )
335+ except Exception as error : print (f"Warning: failed to delete Ecylt conversation: { error !r} " )
336+
337+
338+ '''MultiProviderIssueReplyBot'''
339+ class MultiProviderIssueReplyBot (G4FIssueReplyBot ):
340+ def __init__ (self ) -> None :
341+ super (MultiProviderIssueReplyBot , self ).__init__ ()
342+ self .openai_bot = OpenAICompatibleIssueReplyBot ()
343+ self .ecylt_bot = EcyltFreeGPTIssueReplyBot ()
344+ '''generatereply'''
345+ def generatereply (self , issue : dict [str , Any ], repository_context : str ) -> str :
346+ provider_attempts , errors = [("OpenAI-compatible API" , self .tryopenaicompatible ), ("Ecylt Free GPT API" , self .tryecylt ), ("G4F" , self .tryg4f )], []
347+ for provider_name , provider_call in provider_attempts :
348+ try : print (f"Trying provider: { provider_name } " ); reply = provider_call (issue , repository_context ); print (f"Provider succeeded: { provider_name } " ); return reply
349+ except Exception as error : error_message = f"{ provider_name } : { repr (error )} " ; errors .append (error_message ); print (f"Provider failed: { error_message } " ); traceback .print_exc ()
350+ raise RuntimeError ("All providers failed:\n " + "\n " .join (errors ))
351+ '''tryopenaicompatible'''
352+ def tryopenaicompatible (self , issue : dict [str , Any ], repository_context : str ) -> str :
353+ if not self .openai_bot .openaicompatibleenabled (): raise RuntimeError ("OpenAI-compatible API is not configured." )
354+ messages = self .buildmessages (issue , repository_context )
355+ return self .openai_bot .generatewithopenaicompatible (messages )
356+ '''tryecylt'''
357+ def tryecylt (self , issue : dict [str , Any ], repository_context : str ) -> str :
358+ if not self .ecylt_bot .ecyltenabled (): raise RuntimeError ("Ecylt Free GPT API is disabled." )
359+ messages = self .buildmessages (issue , repository_context )
360+ return self .ecylt_bot .generatewithecylt (messages )
361+ '''tryg4f'''
362+ def tryg4f (self , issue : dict [str , Any ], repository_context : str ) -> str :
363+ return super ().generatereply (issue , repository_context )
364+
365+
271366'''main'''
272367def main () -> None :
273- bot = G4FIssueReplyBot ()
368+ bot = MultiProviderIssueReplyBot ()
274369 bot .run ()
275370
276371
0 commit comments