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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ dependencies {
implementation 'org.json:json:20250107'
implementation "com.google.guava:guava:33.2.0-jre"
implementation group: 'com.fifesoft', name: 'rsyntaxtextarea', version: '3.5.2'
implementation('ai.reveng:sdk:3.86.3')
implementation('ai.reveng:sdk:3.93.2')
testImplementation('junit:junit:4.13.1')
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.8.2")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,12 @@ private void waitForDecomp(TypedApiInterface.FunctionID id, TaskMonitor monitor)
var api = service.getApi();
AIDecompilationStatus lastDecompStatus = null;
boolean inlineCommentsTriggered = false;
boolean summaryTriggered = false;
while (true) {
var newStatus = api.pollAIDecompileStatus(id);
if (lastDecompStatus == null
|| !Objects.equals(newStatus.status(), lastDecompStatus.status())
|| !Objects.equals(newStatus.summaryStatus(), lastDecompStatus.summaryStatus())
|| !Objects.equals(newStatus.inlineCommentsStatus(), lastDecompStatus.inlineCommentsStatus())
|| newStatus.inlineComments().size() != lastDecompStatus.inlineComments().size()) {
lastDecompStatus = newStatus;
Expand Down Expand Up @@ -414,10 +416,26 @@ private void waitForDecomp(TypedApiInterface.FunctionID id, TaskMonitor monitor)
logger.error("Inline comments generation failed for function %s".formatted(functionWithID.function().getName()));
return;
}
if (!inlineCommentsTriggered) {
// Trigger on first entry to COMPLETED regardless of reported status: if comments
// haven't been requested (or the status endpoint was unreachable), POSTing kicks
// them off; if they're already running the server treats it as a regenerate.
var summaryStatus = newStatus.summaryStatus();
if (!summaryTriggered) {
// Summary generation is no longer automatic on the create-decompilation call;
// callers must POST to kick it off, like inline comments.
summaryTriggered = true;
try {
api.triggerAIDecompilationSummary(id);
} catch (RuntimeException e) {
logger.error("Failed to trigger summary: %s".formatted(e.getMessage()));
}
}
if (summaryStatus == WorkflowProgress.StatusEnum.FAILED) {
// Server requires a summary to exist before inline comments can be generated;
// if the summary failed there's no point triggering comments.
logger.error("Summary generation failed for function %s; skipping inline comments".formatted(functionWithID.function().getName()));
return;
}
if (summaryStatus == WorkflowProgress.StatusEnum.COMPLETED && !inlineCommentsTriggered) {
// Gate the inline-comments POST on summary completion — the server rejects it
// with HTTP 400 otherwise ("A summary is required before inline comments can be generated").
inlineCommentsTriggered = true;
try {
api.triggerAIDecompilationInlineComments(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ public Optional<FunctionDataTypeStatus> getFunctionDataTypes(AnalysisID analysis
public boolean triggerAIDecompilationForFunctionID(FunctionID functionID) {
try {
// POST /v3/functions/{function_id}/ai-decompilation
var result = functionsAiDecompilationApi.createAiDecompilation(functionID.value(), true, null);
var result = functionsAiDecompilationApi.createAiDecompilation(functionID.value(), false, null);
return Boolean.TRUE.equals(result.getStatus());
} catch (ApiException e) {
throw new RuntimeException("Failed to trigger AI decompilation", e);
Expand All @@ -459,9 +459,17 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) {
DecompilationData data = functionsAiDecompilationApi.getAiDecompilation(functionID.value());
String summary = null;
String predictedFunctionName = null;
WorkflowProgress.StatusEnum summaryStatus = null;
WorkflowProgress.StatusEnum inlineCommentsStatus = null;
List<AIDecompilationStatus.InlineCommentEntry> inlineComments = List.of();
if (data.getStatus() == DecompilationData.StatusEnum.COMPLETED) {
try {
// GET /v3/functions/{function_id}/ai-decompilation/summary/status
WorkflowProgress summaryProgress = functionsAiDecompilationApi.getAiDecompilationSummaryStatus(functionID.value());
summaryStatus = summaryProgress.getStatus();
} catch (ApiException | RuntimeException e) {
Msg.info(this, "Could not fetch summary status for function " + functionID.value() + ": " + e.getMessage());
}
try {
// GET /v3/functions/{function_id}/ai-decompilation/summary
SummaryData summaryData = functionsAiDecompilationApi.getAiDecompilationSummary(functionID.value());
Expand All @@ -473,9 +481,7 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) {
// GET /v3/functions/{function_id}/ai-decompilation/tokenised — carries the predicted name
TokenisedData tokenised = functionsAiDecompilationApi.getAiDecompilationTokenised(functionID.value());
predictedFunctionName = tokenised.getPredictedFunctionName();
} catch (ApiException | RuntimeException e) {
// RuntimeException covers IllegalArgumentException thrown by the SDK's JSON validator
// when the live response omits fields the generated model marks required (e.g. name_map).
} catch (ApiException e) {
Msg.info(this, "Could not fetch predicted function name for function " + functionID.value() + ": " + e.getMessage());
}
try {
Expand Down Expand Up @@ -506,6 +512,7 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) {
data.getDecompilation(),
summary,
predictedFunctionName,
summaryStatus,
inlineCommentsStatus,
inlineComments);
} catch (ApiException e) {
Expand All @@ -519,10 +526,26 @@ public void triggerAIDecompilationInlineComments(FunctionID functionID) {
// POST /v3/functions/{function_id}/ai-decompilation/inline-comments
functionsAiDecompilationApi.regenerateAiDecompilationInlineComments(functionID.value());
} catch (ApiException e) {
throw new RuntimeException("Failed to trigger AI decompilation inline comments", e);
throw new RuntimeException("Failed to trigger AI decompilation inline comments: " + describeApiException(e), e);
}
}

@Override
public void triggerAIDecompilationSummary(FunctionID functionID) {
try {
// POST /v3/functions/{function_id}/ai-decompilation/summary
functionsAiDecompilationApi.regenerateAiDecompilationSummary(functionID.value());
} catch (ApiException e) {
throw new RuntimeException("Failed to trigger AI decompilation summary: " + describeApiException(e), e);
}
}

private static String describeApiException(ApiException e) {
// The SDK's ApiException carries the HTTP status and response body separately from the message;
// surface both so callers logging only getMessage() can still diagnose server-side failures.
return "HTTP " + e.getCode() + " — " + (e.getResponseBody() != null ? e.getResponseBody() : e.getMessage());
}

/**
* https://api.reveng.ai/v2/docs#tag/Functions-overview/operation/rename_function_id_v2_functions_rename__function_id__post
*
Expand Down Expand Up @@ -662,7 +685,7 @@ public List<CollectionSearchResult> searchCollections(String partialCollectionNa

@Override
public List<BinarySearchResult> searchBinaries(String partialBinaryName, String modelName) throws ApiException {
return this.searchApi.searchBinaries(1, 10, partialBinaryName, null, null, modelName, null).getData().getResults();
return this.searchApi.searchBinaries(1, 10, partialBinaryName, null, null, modelName, null, null).getData().getResults();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ default void triggerAIDecompilationInlineComments(FunctionID functionID) {
throw new UnsupportedOperationException("triggerAIDecompilationInlineComments not implemented yet");
}

default void triggerAIDecompilationSummary(FunctionID functionID) {
throw new UnsupportedOperationException("triggerAIDecompilationSummary not implemented yet");
}

void renameFunction(FunctionID id, String newName, String newNameMangled);

default FunctionNameScore getNameScore(FunctionMatch match) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
* endpoints; this record stitches them back together so callers can treat
* polling as a single operation.
*
* `summary`, `predictedFunctionName`, `inlineCommentsStatus` and
* `inlineComments` are only populated once `status` reaches `COMPLETED`.
* Summary/inline-comments fields are only populated once `status` reaches
* `COMPLETED`. The server rejects an inline-comments trigger until the
* summary has been generated, so consumers must gate that POST on
* `summaryStatus == COMPLETED`.
*/
public record AIDecompilationStatus(
DecompilationData.StatusEnum status,
@Nullable String decompilation,
@Nullable String summary,
@Nullable String predictedFunctionName,
@Nullable WorkflowProgress.StatusEnum summaryStatus,
@Nullable WorkflowProgress.StatusEnum inlineCommentsStatus,
List<InlineCommentEntry> inlineComments
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package ai.reveng.toolkit.ghidra.core.services.api.types.binsync;

import ai.reveng.model.FunctionTypeOutput;
import org.json.JSONObject;

import java.util.ArrayList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public record FunctionDependencies(
Struct[] structs
) {

public static FunctionDependencies fromOpenAPI(List<FunctionInfoInputFuncDepsInner> deps) {
public static FunctionDependencies fromOpenAPI(List<FuncDepsInner> deps) {
if (deps.isEmpty()) {
return null;
}
Expand Down
10 changes: 7 additions & 3 deletions src/test/java/ai/reveng/AIDecompilerComponentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ai.reveng.invoker.ApiException;
import ai.reveng.model.DecompilationData;
import ai.reveng.model.WorkflowProgress;
import ai.reveng.toolkit.ghidra.binarysimilarity.ui.aidecompiler.AIDecompilationdWindow;
import ai.reveng.toolkit.ghidra.core.services.api.AnalysisOptionsBuilder;
import ai.reveng.toolkit.ghidra.core.services.api.mocks.UnimplementedAPI;
Expand Down Expand Up @@ -68,15 +69,17 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) {
"int func2(int a) { return a + 1; }",
"Mocked Description Summary for func2",
null,
null,
WorkflowProgress.StatusEnum.COMPLETED,
WorkflowProgress.StatusEnum.COMPLETED,
java.util.List.of());
} else if (functionID.value() == 1) {
return new AIDecompilationStatus(
DecompilationData.StatusEnum.COMPLETED,
"void func1() { return; }",
"Mocked Description Summary",
null,
null,
WorkflowProgress.StatusEnum.COMPLETED,
WorkflowProgress.StatusEnum.COMPLETED,
java.util.List.of());
} else {
throw new RuntimeException("Unknown FunctionID");
Expand Down Expand Up @@ -230,7 +233,8 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) {
"void func1() { return; }",
"Mocked Description Summary",
null,
null,
WorkflowProgress.StatusEnum.COMPLETED,
WorkflowProgress.StatusEnum.COMPLETED,
java.util.List.of());
}

Expand Down
1 change: 0 additions & 1 deletion src/test/java/ai/reveng/PortalAnalysisIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import ai.reveng.model.FunctionDataTypesList;
import ai.reveng.model.FunctionDataTypesListItem;
import ai.reveng.model.FunctionInfoOutput;
import ai.reveng.model.FunctionTypeOutput;
import ai.reveng.toolkit.ghidra.core.RevEngAIAnalysisResultsLoaded;
import ai.reveng.toolkit.ghidra.core.RevEngAIAnalysisStatusChangedEvent;
import ai.reveng.toolkit.ghidra.core.services.api.AnalysisOptionsBuilder;
Expand Down
Loading