Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 52 additions & 124 deletions src/pyob/autoreviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,42 +198,69 @@ def get_valid_edit(

attempts: int = int(0)
use_ollama = False
is_cloud = (
os.environ.get("GITHUB_ACTIONS") == "true"
or os.environ.get("CI") == "true"
or "GITHUB_RUN_ID" in os.environ
)

while True:
key = None
now = time.time()
available_keys = [
k for k, cooldown in self.key_cooldowns.items() if now > cooldown
]

if not available_keys:
if not use_ollama:
if is_cloud:
logger.warning(
"🚫 All Gemini keys are currently rate-limited. Falling back to Local Ollama."
"☁️ Cloud environment: Gemini keys exhausted/limited. Sleeping 60s for refill..."
)
use_ollama = True
time.sleep(60)
attempts += 1
continue
else:
if not use_ollama:
logger.warning(
"🚫 Gemini rate-limited. Falling back to Local Ollama."
)
use_ollama = True
else:
use_ollama = False
key = available_keys[attempts % len(available_keys)]
logger.info(
f"\n[Attempting Gemini API Key {attempts % len(available_keys) + 1}/{len(available_keys)} Available]"
)

if use_ollama:
logger.info("\n[Attempting Local Ollama]")

response_text = self._stream_single_llm(
prompt, key=key, context=display_name
)

if "ERROR_CODE_413" in response_text:
logger.warning(
"⚠️ GitHub Models context too large (413). Sleeping 60s..."
)
time.sleep(60)
attempts += 1
continue

if response_text.startswith("ERROR_CODE_429"):
if key:
logger.warning(
"⚠️ Key hit a 429 rate limit. Putting it in a 20-minute timeout."
)
logger.warning("⚠️ Key hit a 429 rate limit. Timeout 20m.")
self.key_cooldowns[key] = time.time() + 1200
time.sleep(60)
attempts += 1
continue

if response_text.startswith("ERROR_CODE_") or not response_text.strip():
logger.warning("⚠️ API Error or Empty Response. Rotating...")
logger.warning("⚠️ API Error or Empty Response. Backing off 60s...")
time.sleep(60)
attempts += 1
continue

new_code, explanation, edit_success = self.apply_xml_edits(
source_code, response_text
)
Expand All @@ -251,45 +278,34 @@ def get_valid_edit(
"🤖 AI stated the code looks good, but hallucinated empty <EDIT> blocks. Ignoring them."
)
return source_code, explanation, response_text

if edit_count > 0 and not edit_success:
logger.warning(
f"⚠️ Partial edit failure in {display_name} ({edit_count} blocks found, but some missed targets). Auto-regenerating..."
f"⚠️ Partial edit failure in {display_name}. Auto-regenerating..."
)
time.sleep(60)
time.sleep(30)
attempts += 1
continue

if require_edit and new_code == source_code:
logger.warning("Search block mismatch. Rotating...")
time.sleep(60)
time.sleep(30)
attempts += 1
continue

if not require_edit and new_code == source_code:
lower_exp = explanation.lower()
if (
"no fixes needed" in lower_exp
or "looks good" in lower_exp
or "no changes needed" in lower_exp
):
if ai_approved_code:
return new_code, explanation, response_text
else:
if edit_count > 0:
logger.warning(
f"⚠️ AI generated {edit_count} <EDIT> blocks, but <SEARCH> text failed to match. Rotating..."
)
else:
logger.warning(
f"⚠️ AI provided no edit and didn't state: [{display_name}] looks good. Rotating..."
)
time.sleep(60)
logger.warning("⚠️ AI provided no edit and no approval. Rotating...")
time.sleep(30)
attempts += 1
continue

if new_code != source_code:
print("\n" + "=" * 50)
print(f"💡 AI Proposed Edit Ready for: [{display_name}]")
print("=" * 50)
print(
f"AI Thought: {explanation[:400]}{'...' if len(explanation) > 400 else ''}"
)
diff_lines = list(
difflib.unified_diff(
source_code.splitlines(keepends=True),
Expand All @@ -298,7 +314,6 @@ def get_valid_edit(
tofile="Proposed",
)
)
print("\nProposed Changes:\n")
for line in diff_lines[2:22]:
clean_line = line.rstrip()
if clean_line.startswith("+"):
Expand All @@ -309,39 +324,26 @@ def get_valid_edit(
print(f"\033[94m{clean_line}\033[0m")
else:
print(clean_line)
if len(diff_lines) > 22:
print(f"\033[93m... and {len(diff_lines) - 22} more lines.\033[0m")

user_choice = self.get_user_approval(
"Hit ENTER to APPLY, type 'FULL_DIFF' to view full diff, 'EDIT_CODE' to refine code manually, 'EDIT_XML' to refine AI XML, 'REGENERATE' to retry AI, or 'SKIP' to cancel.",
"Hit ENTER to APPLY, type 'FULL_DIFF', 'EDIT_CODE', 'EDIT_XML', 'REGENERATE', or 'SKIP'.",
timeout=220,
)

if user_choice == "SKIP":
logger.info("AI proposed edit skipped by user.")
return source_code, "Edit skipped by user.", ""
elif user_choice == "REGENERATE":
logger.info("Regenerating AI edit...")
attempts += 1
continue
elif user_choice == "EDIT_XML":
logger.info(
"Opening AI XML response in editor for manual refinement..."
)
response_text = self._edit_prompt_with_external_editor(
response_text
)
new_code, explanation, _ = self.apply_xml_edits(
source_code, response_text
)
if new_code == source_code:
logger.warning(
"Edited XML failed to match code. Skipping edit."
)
return source_code, "Edit failed after manual refinement.", ""
return new_code, explanation, response_text
elif user_choice == "EDIT_CODE":
logger.info(
"Opening proposed code in editor for manual refinement..."
)
file_ext = (
os.path.splitext(target_filepath)[1]
if target_filepath
Expand All @@ -350,86 +352,12 @@ def get_valid_edit(
edited_code = self._launch_external_code_editor(
new_code, file_suffix=file_ext
)
if edited_code == new_code:
logger.info(
"No changes made in external editor. Proceeding with original AI proposal."
)
return new_code, explanation, response_text
else:
logger.info(
"User manually refined code. Applying refined code."
)
return (
edited_code,
explanation + " (User refined code manually)",
response_text,
)
elif user_choice == "FULL_DIFF":
full_diff_text = "".join(diff_lines)
try:
pager_cmd = os.environ.get("PAGER", "less -R").split()
if sys.platform == "win32":
pager_cmd = ["more"]
process = subprocess.Popen(
pager_cmd,
stdin=subprocess.PIPE,
stdout=sys.stdout,
stderr=sys.stderr,
text=True,
)
process.communicate(input=full_diff_text)
except FileNotFoundError:
for line in diff_lines:
clean_line = line.rstrip()
if clean_line.startswith("+"):
print(f"\033[92m{clean_line}\033[0m")
elif clean_line.startswith("-"):
print(f"\033[91m{clean_line}\033[0m")
elif clean_line.startswith("@@"):
print(f"\033[94m{clean_line}\033[0m")
else:
print(clean_line)
user_choice_after_diff = self.get_user_approval(
"Hit ENTER to APPLY, type 'EDIT_CODE' to refine code manually, 'EDIT_XML' to refine AI XML, 'REGENERATE' to retry AI, or 'SKIP' to cancel.",
timeout=220,
return (
edited_code,
explanation + " (User refined code manually)",
response_text,
)
if user_choice_after_diff == "SKIP":
return source_code, "Edit skipped by user.", ""
elif user_choice_after_diff == "REGENERATE":
attempts += 1
continue
elif user_choice_after_diff == "EDIT_XML":
response_text = self._edit_prompt_with_external_editor(
response_text
)
new_code, explanation, _ = self.apply_xml_edits(
source_code, response_text
)
if new_code == source_code:
return (
source_code,
"Edit failed after manual refinement.",
"",
)
return new_code, explanation, response_text
elif user_choice_after_diff == "EDIT_CODE":
file_ext = (
os.path.splitext(target_filepath)[1]
if target_filepath
else ".py"
)
edited_code = self._launch_external_code_editor(
new_code, file_suffix=file_ext
)
if edited_code == new_code:
return new_code, explanation, response_text
else:
return (
edited_code,
explanation + " (User refined code manually)",
response_text,
)
return new_code, explanation, response_text

return new_code, explanation, response_text

def scan_directory(self) -> list[str]:
Expand Down
9 changes: 4 additions & 5 deletions src/pyob/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,8 @@ def on_chunk():
# Immediately intercept 413, pause 60s, and force Gemini usage so outer loops don't panic
if response_text and "413" in response_text:
first_chunk_received[0] = True
sys.stdout.write("\r\033[K")
sys.stdout.flush()
logger.warning(
"\n⚠️ Payload too large for GitHub Models (413). Sleeping 60s, then pivoting to Gemini..."
"\n⚠️ Payload too large. Sleeping 60s, then pivoting to Gemini..."
)
time.sleep(60)
gemini_keys = [
Expand All @@ -227,9 +225,10 @@ def on_chunk():
if k.strip()
]
if gemini_keys:
response_text = stream_gemini(prompt, gemini_keys[0], on_chunk)
# Return a specific signal string so the caller knows it worked
return stream_gemini(prompt, gemini_keys[0], on_chunk)
else:
response_text = "ERROR_CODE_413_NO_GEMINI_FALLBACK"
return "ERROR_CODE_413_NO_GEMINI_FALLBACK"

# Force mandatory sleep if ANY cloud error escapes, breaking infinite loop triggers
if response_text and response_text.startswith("ERROR_CODE_"):
Expand Down
Loading