Skip to content

Commit 888ca3f

Browse files
authored
Merge pull request #13 from RevEngAI/feat-PLU-288
feat(PLU-288): v3 ai-decomp endpoint usage
2 parents a09d352 + e23c56e commit 888ca3f

4 files changed

Lines changed: 191 additions & 126 deletions

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "plugin-binary-ninja"
77
version = "0.0.1"
88
requires-python = ">=3.10"
99
dependencies = [
10-
"revengai>=3.88.0",
10+
"revengai>=3.93.0",
1111
"urllib3>=2.0.0,<2.3.0",
1212
"libbs==2.15.1",
1313
"pydantic>=2.12.5",

‎reai_toolkit/features/ai_decompiler/ai_decompiler.py‎

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Dict, Optional, Callable
22
from binaryninja import BinaryView, log_info, log_error
3-
import revengai
43
from reai_toolkit.utils import get_function_id_by_addr as get_function_id_by_addr_util, AddressChangeMonitor, AIDecompilerChecker
54

65
class AIDecompiler:
@@ -75,68 +74,27 @@ def address_change_callback(self, context, view, addr, change_type):
7574
self.dialog.pre_tab_setup(bv, addr)
7675

7776
def start_ai_decompiler(self, bv: BinaryView, options: Dict) -> None:
77+
editor = options.get("editor")
78+
tab_name = options.get("tab_name")
79+
function = options.get("function")
80+
callback = options.get("callback")
7881
try:
7982
if not self.tracking_enabled:
8083
self.start_address_tracking(self.address_change_callback)
8184
self.tracking_enabled = True
82-
83-
log_info("RevEng.AI | Starting function searching in portal")
84-
editor = options.get("editor")
85-
tab_name = options.get("tab_name")
86-
function = options.get("function")
87-
callback = options.get("callback")
85+
8886
analysis_id = self.config.get_analysis_id(bv)
8987
if not analysis_id:
9088
raise Exception("Analysis not found. Please choose one using the 'Attach to existing' feature.")
9189
function_id = get_function_id_by_addr_util(bv, function.start, self.config)
9290

93-
with self.config.create_api_client() as api_client:
94-
api_instance = revengai.FunctionsAIDecompilationApi(api_client)
95-
api_response_status = api_instance.get_ai_decompilation_task_status(function_id)
96-
log_info(f"RevEng.AI | AI Decompilation task created for function at 0x{function.start:x}")
97-
98-
poll_status = api_response_status.data.status
99-
if not poll_status:
100-
raise Exception("AI Decompilation task not found.")
101-
102-
log_info(f"RevEng.AI | Polling AI decompilation: {poll_status}")
103-
104-
if poll_status.lower() != "completed" and poll_status.lower() != "failed":
105-
log_info(f"RevEng.AI | Starting AI Decompilation for function at 0x{function.start:x}")
106-
107-
if poll_status.lower() == "uninitialised":
108-
try:
109-
with self.config.create_api_client() as api_client:
110-
api_instance = revengai.FunctionsAIDecompilationApi(api_client)
111-
api_response_status = api_instance.create_ai_decompilation_task(function_id)
112-
log_info(f"RevEng.AI | AI Decompilation task created for function at 0x{function.start:x}")
113-
if not api_response_status.status:
114-
callback(editor, "AI Decompilation failed.")
115-
return
116-
117-
except Exception as e:
118-
log_error(f"RevEng.AI | Error beginning AI decompilation: {str(e)}")
119-
callback(editor, "AI Decompilation failed.")
120-
return
121-
122-
log_info("RevEng.AI | AI Decompilation started")
123-
periodic_checker = AIDecompilerChecker()
124-
periodic_checker.start_ai_decompiler_checking(function_id, callback, editor, tab_name, self.config)
125-
self._current_checker = periodic_checker
126-
127-
if poll_status.lower() == "completed":
128-
log_info(f"RevEng.AI | AI Decompilation for function at 0x{function.start:x} is completed")
129-
with self.config.create_api_client() as api_client:
130-
api_instance = revengai.FunctionsAIDecompilationApi(api_client)
131-
api_response_status = api_instance.get_ai_decompilation_task_result(function_id, summarise=True, generate_inline_comments=True)
132-
133-
callback(editor, api_response_status.data.decompilation)
134-
135-
if poll_status.lower() == "failed":
136-
log_info(f"RevEng.AI | AI Decompilation for function at 0x{function.start:x} failed")
137-
callback(editor, "AI Decompilation failed.")
91+
log_info(f"RevEng.AI | Starting AI decompilation for function at 0x{function.start:x}")
92+
periodic_checker = AIDecompilerChecker()
93+
periodic_checker.start_ai_decompiler_checking(function_id, callback, editor, tab_name, self.config)
94+
self._current_checker = periodic_checker
13895

13996
except Exception as e:
14097
log_error(f"RevEng.AI | Error in AI decompiler: {str(e)}")
141-
callback(editor, "AI Decompilation failed.")
98+
if callback and editor:
99+
callback(editor, f"AI Decompilation failed: {e}")
142100
return False, str(e)

‎reai_toolkit/utils/monitoring/ai_decompiler_monitor.py‎

Lines changed: 175 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,58 @@
55
from PySide6.QtWidgets import QPlainTextEdit
66
from PySide6.QtCore import QTimer, QObject, Signal
77

8+
_STATUS_UNINITIALISED = "UNINITIALISED"
9+
_STATUS_COMPLETED = "COMPLETED"
10+
_STATUS_FAILED = "FAILED"
11+
12+
_PHASE_DECOMP = "decomp"
13+
_PHASE_SUMMARY = "summary"
14+
_PHASE_COMMENTS = "comments"
15+
_PHASE_DONE = "done"
16+
17+
18+
def _normalise_status(status) -> str:
19+
return str(status).strip().upper()
20+
21+
22+
def _format_summary_as_comment(summary: str) -> str:
23+
body = "\n".join(f" * {line}" for line in summary.strip().splitlines())
24+
return f"/*\n{body}\n */"
25+
26+
27+
def _inject_inline_comments(code: str, comments) -> str:
28+
lines = code.split("\n")
29+
for c in sorted(comments, key=lambda x: x.line, reverse=True):
30+
idx = c.line - 1
31+
if idx < 0 or idx >= len(lines):
32+
continue
33+
target = lines[idx]
34+
indent = target[: len(target) - len(target.lstrip())]
35+
lines.insert(idx, f"{indent}// {c.comment}")
36+
return "\n".join(lines)
37+
38+
839
class AIDecompilerChecker(QObject):
9-
# Signal for thread-safe UI updates
1040
update_text_signal = Signal(object, str)
11-
41+
1242
def __init__(self):
1343
super().__init__()
1444
self._current_timer: Optional[Timer] = None
1545
self._ai_decompiler_timer: Optional[QTimer] = None
1646
self.number_of_clicks = 0
1747
self.flag = False
18-
# Connect signal to slot for safe UI updates
48+
self._function_id: Optional[int] = None
49+
self._phase = _PHASE_DECOMP
50+
self._decompilation: Optional[str] = None
51+
self._summary_text: Optional[str] = None
52+
self._inline_comments = None
53+
self._summary_requeued = False
54+
self._comments_requeued = False
1955
self.update_text_signal.connect(self._update_text_slot)
2056

2157
def _update_text_slot(self, callback, text):
22-
"""Slot that runs in the main thread to safely update UI"""
2358
try:
2459
if hasattr(callback, '__call__'):
25-
# If callback is a function, call it with editor and text
26-
# We need to get the editor from somewhere - let's store it
2760
if hasattr(self, '_current_editor'):
2861
callback(self._current_editor, text)
2962
except Exception as ex:
@@ -34,93 +67,167 @@ def stop(self):
3467
self._current_timer.cancel()
3568
self._current_timer = None
3669
log_info("RevEng.AI | Stopped periodic status check")
37-
70+
3871
if self._ai_decompiler_timer:
3972
self._ai_decompiler_timer.stop()
4073
self._ai_decompiler_timer = None
4174
log_info("RevEng.AI | Stopped AI decompiler periodic check")
4275

4376
def start_ai_decompiler_checking(self, function_id: int, callback, editor: QPlainTextEdit, name: str, config) -> None:
44-
"""Start periodic checking for AI decompiler with thread-safe UI updates"""
4577
try:
46-
47-
# Store editor reference for the signal slot
4878
log_info(f"RevEng.AI | Starting AI decompiler periodic check for: {name}")
4979
self._current_editor = editor
5080
self._current_callback = callback
5181
self._current_config = config
52-
# Stop any existing timer
82+
83+
self._function_id = function_id
84+
self._phase = _PHASE_DECOMP
85+
self._decompilation = None
86+
self._summary_text = None
87+
self._inline_comments = None
88+
self._summary_requeued = False
89+
self._comments_requeued = False
90+
5391
if self._ai_decompiler_timer:
5492
self._ai_decompiler_timer.stop()
55-
56-
# Create QTimer for thread-safe execution
93+
5794
self._ai_decompiler_timer = QTimer()
58-
self._ai_decompiler_timer.timeout.connect(lambda: self._ai_decompiler_worker(function_id, name, callback, editor))
59-
60-
# Start the timer with 5 second intervals
61-
self._ai_decompiler_timer.start(1000) # 1000 ms = 1 second
62-
95+
self._ai_decompiler_timer.timeout.connect(self._ai_decompiler_worker)
96+
self._ai_decompiler_timer.start(1000)
97+
6398
log_info(f"RevEng.AI | Started AI decompiler periodic check for: {name}")
64-
99+
100+
self._ai_decompiler_worker()
101+
65102
except Exception as ex:
66103
log_error(f"RevEng.AI | Error starting AI decompiler check: {str(ex)}")
67-
68-
def _ai_decompiler_worker(self, function_id: int, name: str, callback, editor: QPlainTextEdit):
69-
"""Worker method that runs in a separate thread via QTimer"""
104+
105+
def _api(self, api_client):
106+
return revengai.FunctionsAIDecompilationApi(api_client)
107+
108+
def _render(self):
109+
parts = []
110+
if self._summary_text:
111+
parts.append(_format_summary_as_comment(self._summary_text))
112+
113+
body = self._decompilation or ""
114+
if self._inline_comments:
115+
body = _inject_inline_comments(body, self._inline_comments)
116+
117+
text = "\n".join(parts + [body]) if parts else body
118+
self.update_text_signal.emit(self._current_callback, text)
119+
120+
def _run_decomp_phase(self):
121+
fid = self._function_id
122+
with self._current_config.create_api_client() as api_client:
123+
status = _normalise_status(self._api(api_client).get_ai_decompilation_status(fid).status)
124+
log_info(f"RevEng.AI | AI Decompilation status for Function ID {fid}: {status}")
125+
126+
if status == _STATUS_UNINITIALISED:
127+
with self._current_config.create_api_client() as api_client:
128+
response = self._api(api_client).create_ai_decompilation(fid)
129+
if not getattr(response, "status", False):
130+
raise Exception(f"AI Decompilation for Function ID {fid} failed to start")
131+
log_info(f"RevEng.AI | AI Decompilation task created for Function ID {fid}")
132+
return
133+
134+
if status == _STATUS_FAILED:
135+
raise Exception(f"AI Decompilation for Function ID {fid} failed")
136+
137+
if status != _STATUS_COMPLETED:
138+
return
139+
140+
with self._current_config.create_api_client() as api_client:
141+
self._decompilation = self._api(api_client).get_ai_decompilation(fid).decompilation
142+
log_info(f"RevEng.AI | AI Decompilation for Function ID {fid} completed")
143+
self._render()
144+
self._phase = _PHASE_SUMMARY
145+
146+
def _run_summary_phase(self):
147+
fid = self._function_id
70148
try:
71-
if self.flag:
72-
log_info(f"RevEng.AI | AI Decompilation is already in progress")
149+
with self._current_config.create_api_client() as api_client:
150+
summary = self._api(api_client).get_ai_decompilation_summary(fid)
151+
status = _normalise_status(summary.task_status)
152+
153+
if status == _STATUS_COMPLETED:
154+
self._summary_text = summary.ai_summary
155+
self._render()
156+
self._phase = _PHASE_COMMENTS
157+
return
158+
159+
if status == _STATUS_FAILED:
160+
log_error(f"RevEng.AI | AI summary generation failed for Function ID {fid}")
161+
self._phase = _PHASE_COMMENTS
73162
return
74-
self.flag = True
75163

164+
if status == _STATUS_UNINITIALISED and not self._summary_requeued:
165+
self._requeue(self._api_regenerate_summary, fid, "summary")
166+
self._summary_requeued = True
167+
168+
except Exception as ex:
169+
log_error(f"RevEng.AI | Error fetching AI summary for Function ID {fid}: {str(ex)}")
170+
self._phase = _PHASE_COMMENTS
171+
172+
def _run_comments_phase(self):
173+
fid = self._function_id
174+
try:
76175
with self._current_config.create_api_client() as api_client:
77-
api_instance = revengai.FunctionsAIDecompilationApi(api_client)
78-
api_response_status = api_instance.get_ai_decompilation_task_status(function_id)
79-
log_info(f"RevEng.AI | AI Decompilation task created for Function ID: {function_id}")
80-
81-
poll_status = api_response_status.data.status
82-
if not poll_status:
83-
raise Exception("AI Decompilation task not found.")
84-
85-
log_info(f"RevEng.AI | AI Decompilation for Function ID: {function_id} is {poll_status}")
86-
87-
if poll_status.lower() == "uninitialised":
88-
log_info(f"RevEng.AI | Starting AI Decompilation for Function ID: {function_id}")
89-
try:
90-
with self._current_config.create_api_client() as api_client:
91-
api_instance = revengai.FunctionsAIDecompilationApi(api_client)
92-
api_response = api_instance.create_ai_decompilation_task(function_id)
93-
if not api_response.status:
94-
raise Exception(f"AI Decompilation for Function ID: {function_id} failed")
95-
log_info(f"RevEng.AI | AI Decompilation task created for Function ID: {function_id}")
96-
97-
except Exception as e:
98-
log_error(f"RevEng.AI | Error beginning AI decompilation: {str(e)}")
99-
raise Exception(f"AI Decompilation for Function ID: {function_id} failed")
100-
101-
elif poll_status.lower() == "failed":
102-
raise Exception(f"AI Decompilation for Function ID: {function_id} failed")
103-
104-
elif poll_status.lower() != "completed":
105-
log_info(f"RevEng.AI | AI Decompilation for Function ID: {function_id} is not completed")
106-
self.flag = False
176+
comments = self._api(api_client).get_ai_decompilation_inline_comments(fid)
177+
status = _normalise_status(comments.task_status)
178+
179+
if status == _STATUS_COMPLETED:
180+
self._inline_comments = comments.inline_comments
181+
self._render()
182+
self._finish()
107183
return
108-
109-
self.flag = False
110-
log_info(f"RevEng.AI | AI Decompilation for Function ID: {function_id} is completed")
184+
185+
if status == _STATUS_FAILED:
186+
log_error(f"RevEng.AI | Inline comment generation failed for Function ID {fid}")
187+
self._finish()
188+
return
189+
190+
if status == _STATUS_UNINITIALISED and not self._comments_requeued:
191+
self._requeue(self._api_regenerate_comments, fid, "inline comments")
192+
self._comments_requeued = True
193+
194+
except Exception as ex:
195+
log_error(f"RevEng.AI | Error fetching inline comments for Function ID {fid}: {str(ex)}")
196+
self._finish()
197+
198+
def _api_regenerate_summary(self, api_client, fid):
199+
self._api(api_client).regenerate_ai_decompilation_summary(fid)
200+
201+
def _api_regenerate_comments(self, api_client, fid):
202+
self._api(api_client).regenerate_ai_decompilation_inline_comments(fid)
203+
204+
def _requeue(self, regenerate_fn, fid, label):
205+
try:
111206
with self._current_config.create_api_client() as api_client:
112-
api_instance = revengai.FunctionsAIDecompilationApi(api_client)
113-
api_response = api_instance.get_ai_decompilation_task_result(function_id, summarise=True, generate_inline_comments=True)
114-
115-
# Safely update UI through signal/slot mechanism
116-
self.update_text_signal.emit(self._current_callback, api_response.data.decompilation)
207+
regenerate_fn(api_client, fid)
208+
log_info(f"RevEng.AI | Requested {label} generation for Function ID {fid}")
209+
except Exception as ex:
210+
log_info(f"RevEng.AI | Could not (re)queue {label} for Function ID {fid}: {str(ex)}")
117211

118-
# Stop timer from main thread
119-
self.update_text_signal.emit(lambda x,y: self.stop(), "")
212+
def _finish(self):
213+
self._phase = _PHASE_DONE
214+
self.update_text_signal.emit(lambda x, y: self.stop(), "")
120215

216+
def _ai_decompiler_worker(self):
217+
if self.flag:
218+
log_info("RevEng.AI | AI Decompilation tick already in progress")
219+
return
220+
self.flag = True
221+
try:
222+
if self._phase == _PHASE_DECOMP:
223+
self._run_decomp_phase()
224+
elif self._phase == _PHASE_SUMMARY:
225+
self._run_summary_phase()
226+
elif self._phase == _PHASE_COMMENTS:
227+
self._run_comments_phase()
121228
except Exception as ex:
122-
self.flag = False
123229
log_error(f"RevEng.AI | Error in AI decompiler worker: {str(ex)}")
124230
self.update_text_signal.emit(self._current_callback, f"AI Decompilation failed: {ex}")
125-
# Stop timer from main thread
126-
self.update_text_signal.emit(lambda x,y: self.stop(), "")
231+
self._finish()
232+
finally:
233+
self.flag = False

0 commit comments

Comments
 (0)