Skip to content

Commit 343bd1b

Browse files
set auto replay robot for issues opened by repo users
1 parent f15661f commit 343bd1b

2 files changed

Lines changed: 98 additions & 1 deletion

File tree

.github/workflows/g4f-issue-reply.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ jobs:
3737
env:
3838
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3939
G4F_MODELS: ${{ vars.G4F_MODELS }}
40+
ECYLT_FREE_GPT_ENABLED: ${{ vars.ECYLT_FREE_GPT_ENABLED || 'true' }}
41+
ECYLT_FREE_GPT_URL: ${{ vars.ECYLT_FREE_GPT_URL || 'https://api.ecylt.top/v1/free_gpt/chat_json.php' }}
4042
OPENAI_COMPATIBLE_API_KEY: ${{ secrets.OPENAI_COMPATIBLE_API_KEY }}
4143
OPENAI_COMPATIBLE_BASE_URL: ${{ vars.OPENAI_COMPATIBLE_BASE_URL }}
4244
OPENAI_COMPATIBLE_MODEL: ${{ vars.OPENAI_COMPATIBLE_MODEL }}

scripts/g4f_issue_reply.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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'''
272367
def main() -> None:
273-
bot = G4FIssueReplyBot()
368+
bot = MultiProviderIssueReplyBot()
274369
bot.run()
275370

276371

0 commit comments

Comments
 (0)