diff --git a/build.gradle b/build.gradle index 2bc7b4d2..c16f953c 100644 --- a/build.gradle +++ b/build.gradle @@ -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") diff --git a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java index f2d8fea1..17020a60 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/binarysimilarity/ui/aidecompiler/AIDecompilationdWindow.java @@ -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; @@ -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); diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java index 9c9e842b..fa24f387 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiImplementation.java @@ -445,7 +445,7 @@ public Optional 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); @@ -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 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()); @@ -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 { @@ -506,6 +512,7 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { data.getDecompilation(), summary, predictedFunctionName, + summaryStatus, inlineCommentsStatus, inlineComments); } catch (ApiException e) { @@ -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 * @@ -662,7 +685,7 @@ public List searchCollections(String partialCollectionNa @Override public List 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 diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java index f3494cba..aa0dcd93 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/TypedApiInterface.java @@ -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) { diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java index 719873aa..d806e3bc 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/AIDecompilationStatus.java @@ -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 inlineComments ) { diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionArtifact.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionArtifact.java index 6b105091..6cb1e6d5 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionArtifact.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionArtifact.java @@ -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; diff --git a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionDependencies.java b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionDependencies.java index 4346766e..c7a620d1 100644 --- a/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionDependencies.java +++ b/src/main/java/ai/reveng/toolkit/ghidra/core/services/api/types/binsync/FunctionDependencies.java @@ -21,7 +21,7 @@ public record FunctionDependencies( Struct[] structs ) { - public static FunctionDependencies fromOpenAPI(List deps) { + public static FunctionDependencies fromOpenAPI(List deps) { if (deps.isEmpty()) { return null; } diff --git a/src/test/java/ai/reveng/AIDecompilerComponentTest.java b/src/test/java/ai/reveng/AIDecompilerComponentTest.java index a6a44072..9ee8bc7a 100644 --- a/src/test/java/ai/reveng/AIDecompilerComponentTest.java +++ b/src/test/java/ai/reveng/AIDecompilerComponentTest.java @@ -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; @@ -68,7 +69,8 @@ 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( @@ -76,7 +78,8 @@ public AIDecompilationStatus pollAIDecompileStatus(FunctionID functionID) { "void func1() { return; }", "Mocked Description Summary", null, - null, + WorkflowProgress.StatusEnum.COMPLETED, + WorkflowProgress.StatusEnum.COMPLETED, java.util.List.of()); } else { throw new RuntimeException("Unknown FunctionID"); @@ -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()); } diff --git a/src/test/java/ai/reveng/PortalAnalysisIntegrationTest.java b/src/test/java/ai/reveng/PortalAnalysisIntegrationTest.java index 6b918b7f..4178d420 100644 --- a/src/test/java/ai/reveng/PortalAnalysisIntegrationTest.java +++ b/src/test/java/ai/reveng/PortalAnalysisIntegrationTest.java @@ -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;