Compare commits
4 Commits
dd19eb6a76
...
6ad98e9675
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ad98e9675 | |||
| 0c54602fcf | |||
| 75275f992d | |||
| 7c721cd75f |
@ -12,5 +12,4 @@ uploads/
|
||||
.env
|
||||
.env.example
|
||||
docker-compose.yml
|
||||
Dockerfile*
|
||||
engines/
|
||||
|
||||
29
.env.example
29
.env.example
@ -4,18 +4,27 @@
|
||||
# Copy this file to .env and fill in your values.
|
||||
# .env is git-ignored and never committed.
|
||||
|
||||
# --- LLM Provider ---
|
||||
|
||||
# Default chat provider (ollama)
|
||||
AI_DEFAULT_CHAT_PROVIDER=ollama
|
||||
|
||||
# --- Model overrides (optional) ---
|
||||
# --- LLM (Ollama local) ---
|
||||
|
||||
# Ollama chat model (default: gemma4:12b)
|
||||
# AI_CHAT_MODEL_OLLAMA=gemma4:12b
|
||||
|
||||
# Ollama embedding model (default: qwen3-embedding:latest)
|
||||
# AI_EMBEDDING_MODEL=qwen3-embedding:latest
|
||||
# Ollama embedding model (default: nomic-embed-text)
|
||||
# AI_EMBEDDING_MODEL=nomic-embed-text
|
||||
|
||||
# Embedding vector dimension (default: 4096)
|
||||
# AI_EMBEDDING_DIMENSION=4096
|
||||
# Embedding vector dimension (default: 768)
|
||||
# AI_EMBEDDING_DIMENSION=768
|
||||
|
||||
# --- Cloud Chat (Ollama Cloud, DeepSeek, etc.) ---
|
||||
|
||||
# Chat provider: "ollama" (local, default), "ollama-cloud", or "deepseek"
|
||||
# AI_DEFAULT_CHAT_PROVIDER=ollama
|
||||
|
||||
# Cloud API key (required for ollama-cloud or deepseek)
|
||||
# AI_CLOUD_API_KEY=sk-...
|
||||
|
||||
# Cloud base URL (Ollama Cloud: https://api.ollama.ai/v1 | DeepSeek: https://api.deepseek.com)
|
||||
# AI_CLOUD_BASE_URL=https://api.ollama.ai/v1
|
||||
|
||||
# Cloud chat model (Ollama Cloud: gemma4:31b-cloud | DeepSeek: deepseek-chat)
|
||||
# AI_CLOUD_CHAT_MODEL=gemma4:31b-cloud
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
services:
|
||||
backend:
|
||||
profiles:
|
||||
- all
|
||||
container_name: decompile-ai-backend
|
||||
build: .
|
||||
ports:
|
||||
@ -20,6 +22,9 @@ services:
|
||||
- decompile-ai-network
|
||||
|
||||
postgres:
|
||||
profiles:
|
||||
- dev
|
||||
- all
|
||||
container_name: decompile-ai-postgres
|
||||
image: pgvector/pgvector:pg18
|
||||
environment:
|
||||
@ -39,6 +44,9 @@ services:
|
||||
- decompile-ai-network
|
||||
|
||||
rabbitmq:
|
||||
profiles:
|
||||
- dev
|
||||
- all
|
||||
container_name: decompile-ai-rabbitmq
|
||||
image: rabbitmq:4-management
|
||||
ports:
|
||||
|
||||
10
pom.xml
10
pom.xml
@ -151,10 +151,19 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-client-chat</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>me.paulschwarz</groupId>
|
||||
<artifactId>spring-dotenv</artifactId>
|
||||
<version>5.1.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
@ -193,6 +202,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.15.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-compile</id>
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
package ai.decompile.ai.config;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import java.time.Duration;
|
||||
import org.springframework.ai.ollama.OllamaChatModel;
|
||||
import org.springframework.ai.ollama.OllamaEmbeddingModel;
|
||||
import org.springframework.ai.ollama.api.OllamaApi;
|
||||
import org.springframework.ai.ollama.api.OllamaChatOptions;
|
||||
import org.springframework.ai.ollama.api.OllamaEmbeddingOptions;
|
||||
import org.springframework.ai.ollama.management.ModelManagementOptions;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@ -29,12 +33,25 @@ public class AiConfig {
|
||||
@Value("${spring.ai.ollama.embedding.options.model:qwen3-embedding:latest}")
|
||||
private String ollamaEmbeddingModel;
|
||||
|
||||
@Value("${AI_CLOUD_API_KEY:}")
|
||||
private String cloudApiKey;
|
||||
|
||||
@Value("${AI_CLOUD_BASE_URL:http://localhost:11434/v1}")
|
||||
private String cloudBaseUrl;
|
||||
|
||||
@Value("${AI_CLOUD_CHAT_MODEL:gemma4:12b}")
|
||||
private String cloudChatModel;
|
||||
|
||||
@Bean
|
||||
OllamaApi ollamaApi() {
|
||||
return OllamaApi.builder().baseUrl(ollamaBaseUrl).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
name = "app.ai.default-chat-provider",
|
||||
havingValue = "ollama",
|
||||
matchIfMissing = true)
|
||||
OllamaChatModel ollamaChatModel(OllamaApi ollamaApi, ObservationRegistry observationRegistry) {
|
||||
return OllamaChatModel.builder()
|
||||
.ollamaApi(ollamaApi)
|
||||
@ -57,4 +74,20 @@ public class AiConfig {
|
||||
.modelManagementOptions(ModelManagementOptions.defaults())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnExpression(
|
||||
"'${app.ai.default-chat-provider:ollama}' != 'ollama' && '${AI_CLOUD_API_KEY:}' != ''")
|
||||
OpenAiChatModel openAiChatModel(ObservationRegistry observationRegistry) {
|
||||
return OpenAiChatModel.builder()
|
||||
.options(
|
||||
OpenAiChatOptions.builder()
|
||||
.baseUrl(cloudBaseUrl)
|
||||
.apiKey(cloudApiKey)
|
||||
.model(cloudChatModel)
|
||||
.timeout(Duration.ofMinutes(5))
|
||||
.build())
|
||||
.observationRegistry(observationRegistry)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ public class ChatController {
|
||||
public ResponseEntity<SessionResponse> createSession(
|
||||
@PathVariable UUID binaryId,
|
||||
@RequestParam(required = false) UUID projectId,
|
||||
@RequestParam(required = false, defaultValue = "gemma4:12b") String model) {
|
||||
@RequestParam(required = false) String model) {
|
||||
if (projectId == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ public class EmbeddingChunk {
|
||||
private String content;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.VECTOR)
|
||||
@Column(name = "embedding", columnDefinition = "vector(4096)")
|
||||
@Column(name = "embedding", columnDefinition = "vector(768)")
|
||||
private float[] embedding;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
|
||||
@ -51,6 +51,8 @@ public interface EmbeddingChunkRepository extends JpaRepository<EmbeddingChunk,
|
||||
|
||||
void deleteByBinaryId(UUID binaryId);
|
||||
|
||||
void deleteByBinaryIdAndChunkType(UUID binaryId, String chunkType);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("DELETE FROM EmbeddingChunk e WHERE e.sourceId = :sourceId AND e.sourceType = :sourceType")
|
||||
|
||||
@ -30,8 +30,14 @@ public class ChatSessionService {
|
||||
private final StaticAnalysisRepository analysisRepository;
|
||||
private final StreamingModelProvider streamingModelProvider;
|
||||
|
||||
@Value("${app.ai.chat.default-model:gemma4:12b}")
|
||||
private String defaultModel;
|
||||
@Value("${AI_CLOUD_CHAT_MODEL:gemma4:12b}")
|
||||
private String cloudChatModel;
|
||||
|
||||
@Value("${spring.ai.ollama.chat.options.model:gemma4:12b}")
|
||||
private String ollamaChatModel;
|
||||
|
||||
@Value("${app.ai.default-chat-provider:ollama}")
|
||||
private String chatProvider;
|
||||
|
||||
@Transactional
|
||||
public ChatSession createSession(UUID binaryId, UUID projectId, String model) {
|
||||
@ -46,7 +52,7 @@ public class ChatSessionService {
|
||||
+ "' has no static analysis yet. Run static analysis before starting a chat session.");
|
||||
}
|
||||
|
||||
var effectiveModel = Objects.nonNull(model) ? model : defaultModel;
|
||||
var effectiveModel = Objects.nonNull(model) ? model : resolveDefaultModel();
|
||||
streamingModelProvider.resolve();
|
||||
|
||||
var session =
|
||||
@ -60,6 +66,14 @@ public class ChatSessionService {
|
||||
return sessionRepository.save(session);
|
||||
}
|
||||
|
||||
private String resolveDefaultModel() {
|
||||
if ("ollama".equals(chatProvider)) {
|
||||
return ollamaChatModel;
|
||||
}
|
||||
|
||||
return cloudChatModel;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ChatSession> getSessions(UUID binaryId) {
|
||||
return sessionRepository.findByBinaryIdOrderByUpdatedAtDesc(binaryId);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package ai.decompile.ai.service;
|
||||
|
||||
import ai.decompile.ai.model.entity.EmbeddingChunk;
|
||||
import ai.decompile.analysis.model.entity.MutationSuggestion;
|
||||
import ai.decompile.analysis.model.entity.StaticAnalysis;
|
||||
import ai.decompile.analysis.model.entity.StaticDataLabel;
|
||||
import ai.decompile.analysis.model.entity.StaticEnum;
|
||||
@ -87,10 +86,30 @@ public class EmbeddingChunkBuilder {
|
||||
}
|
||||
|
||||
var analysisId = functions.getFirst().getAnalysis().getId();
|
||||
return mutationRepository.findApprovedFunctionRenamesByAnalysisId(analysisId).stream()
|
||||
.collect(
|
||||
java.util.stream.Collectors.toMap(
|
||||
MutationSuggestion::getOldValue, MutationSuggestion::getNewValue, (_, b) -> b));
|
||||
var functionRenames = mutationRepository.findApprovedFunctionRenamesByAnalysisId(analysisId);
|
||||
var labelRenames = mutationRepository.findApprovedLabelRenamesByAnalysisId(analysisId);
|
||||
var globalVarRenames = mutationRepository.findApprovedGlobalVarRenamesByAnalysisId(analysisId);
|
||||
var structRenames = mutationRepository.findApprovedStructRenamesByAnalysisId(analysisId);
|
||||
|
||||
var map = new HashMap<String, String>();
|
||||
|
||||
for (var m : labelRenames) {
|
||||
map.put(m.getOldValue(), m.getNewValue());
|
||||
}
|
||||
|
||||
for (var m : globalVarRenames) {
|
||||
map.put(m.getOldValue(), m.getNewValue());
|
||||
}
|
||||
|
||||
for (var m : structRenames) {
|
||||
map.put(m.getOldValue(), m.getNewValue());
|
||||
}
|
||||
|
||||
for (var m : functionRenames) {
|
||||
map.put(m.getOldValue(), m.getNewValue());
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public EmbeddingChunk buildMetadataChunk(
|
||||
|
||||
@ -133,6 +133,31 @@ public class EmbeddingService {
|
||||
binaryId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void reindexAllFunctions(UUID binaryId) {
|
||||
var analysis = getLatestAnalysis(binaryId);
|
||||
if (Objects.isNull(analysis)) {
|
||||
log.warn("reindexAllFunctions: no analysis found for binary {}, skipping", binaryId);
|
||||
return;
|
||||
}
|
||||
|
||||
var functions = analysis.getFunctions();
|
||||
if (functions.isEmpty()) {
|
||||
log.debug("reindexAllFunctions: no functions for binary {}, nothing to do", binaryId);
|
||||
return;
|
||||
}
|
||||
|
||||
var binary = binaryService.getBinary(binaryId);
|
||||
|
||||
embeddingChunkRepository.deleteByBinaryIdAndChunkType(binaryId, "FUNCTION");
|
||||
embeddingChunkRepository.flush();
|
||||
|
||||
var chunks = chunkBuilder.buildFunctionChunks(functions, binary);
|
||||
embedAndPersistChunks(chunks);
|
||||
|
||||
log.info("Re-indexed all {} function chunks for binary {}", functions.size(), binaryId);
|
||||
}
|
||||
|
||||
private void embedAndPersistChunks(List<EmbeddingChunk> chunks) {
|
||||
for (var i = 0; i < chunks.size(); i += batchSize) {
|
||||
var batch = chunks.subList(i, Math.min(i + batchSize, chunks.size()));
|
||||
|
||||
@ -6,6 +6,7 @@ import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.ai.chat.model.StreamingChatModel;
|
||||
import org.springframework.ai.ollama.OllamaChatModel;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ -13,6 +14,7 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(name = "app.ai.enabled", havingValue = "true", matchIfMissing = true)
|
||||
@ConditionalOnExpression("'${app.ai.default-chat-provider:ollama}' == 'ollama'")
|
||||
public class OllamaStreamingModelProvider implements StreamingModelProvider {
|
||||
private final ObjectProvider<OllamaChatModel> ollamaChatModelProvider;
|
||||
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
package ai.decompile.ai.service;
|
||||
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.ai.chat.model.StreamingChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Log4j2
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(name = "app.ai.enabled", havingValue = "true", matchIfMissing = true)
|
||||
@ConditionalOnExpression("'${app.ai.default-chat-provider:ollama}' != 'ollama'")
|
||||
public class OpenAiStreamingModelProvider implements StreamingModelProvider {
|
||||
private final ObjectProvider<OpenAiChatModel> openAiChatModelProvider;
|
||||
|
||||
@Override
|
||||
public StreamingChatModel resolve() {
|
||||
var model = openAiChatModelProvider.getIfAvailable();
|
||||
if (Objects.isNull(model)) {
|
||||
throw new IllegalStateException(
|
||||
"Cloud chat is not configured. Set AI_CLOUD_API_KEY in your .env file.");
|
||||
}
|
||||
|
||||
log.info("Using cloud chat provider with model: {}", model);
|
||||
return model;
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,21 @@
|
||||
package ai.decompile.analysis.controller;
|
||||
|
||||
import ai.decompile.analysis.model.dto.LibraryExport;
|
||||
import ai.decompile.analysis.model.dto.LibraryResponse;
|
||||
import ai.decompile.analysis.model.dto.LibrarySymbolResponse;
|
||||
import ai.decompile.analysis.service.LibraryService;
|
||||
import ai.decompile.analysis.service.SymbolResolutionService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -47,6 +51,19 @@ public class LibraryController {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(lib);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Import a library from JSON export",
|
||||
description =
|
||||
"Create a new library from a previously exported JSON backup. "
|
||||
+ "The new library gets its own IDs — existing libraries are not affected.",
|
||||
tags = {"Library Symbols"},
|
||||
operationId = "importLibrary")
|
||||
@PostMapping("/libraries/import")
|
||||
public ResponseEntity<LibraryResponse> importLibrary(@RequestBody LibraryExport data) {
|
||||
var lib = libraryService.importFromExport(data);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(lib);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "List available parsers",
|
||||
description = "Retrieve the list of supported library symbol parser formats.",
|
||||
@ -77,6 +94,26 @@ public class LibraryController {
|
||||
return ResponseEntity.ok(libraryService.getLibrary(libraryId));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Export a library as JSON",
|
||||
description =
|
||||
"Download the complete library (name, description, aliases, and all symbols) "
|
||||
+ "as a JSON file for backup or transfer.",
|
||||
tags = {"Library Symbols"},
|
||||
operationId = "exportLibrary")
|
||||
@GetMapping("/libraries/{libraryId}/export")
|
||||
public ResponseEntity<LibraryExport> exportLibrary(@PathVariable UUID libraryId) {
|
||||
var data = libraryService.exportLibrary(libraryId);
|
||||
var filename = sanitizeFilename(data.name()) + ".json";
|
||||
|
||||
var headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setContentDisposition(
|
||||
ContentDisposition.attachment().filename(filename, StandardCharsets.UTF_8).build());
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(data);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Delete a library",
|
||||
description = "Delete a library and all its associated symbols and aliases.",
|
||||
@ -122,4 +159,8 @@ public class LibraryController {
|
||||
var resolvedCount = symbolResolutionService.resolveAllUnresolved();
|
||||
return ResponseEntity.ok(Map.of("resolvedCount", resolvedCount));
|
||||
}
|
||||
|
||||
private String sanitizeFilename(String name) {
|
||||
return name.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
package ai.decompile.analysis.model.dto;
|
||||
|
||||
import ai.decompile.analysis.model.entity.Library;
|
||||
import ai.decompile.analysis.model.entity.LibrarySymbol;
|
||||
import com.fasterxml.jackson.annotation.JsonRootName;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@JsonRootName("library")
|
||||
public record LibraryExport(
|
||||
String name, String description, List<String> aliases, List<SymbolEntry> symbols) {
|
||||
|
||||
public record SymbolEntry(
|
||||
String moduleName,
|
||||
String className,
|
||||
String symbolName,
|
||||
int ordinal,
|
||||
String symbolType,
|
||||
String parameters,
|
||||
String category) {
|
||||
|
||||
static SymbolEntry from(LibrarySymbol sym) {
|
||||
return new SymbolEntry(
|
||||
sym.getModuleName(),
|
||||
sym.getClassName(),
|
||||
sym.getSymbolName(),
|
||||
sym.getOrdinal(),
|
||||
sym.getSymbolType(),
|
||||
sym.getParameters(),
|
||||
sym.getCategory());
|
||||
}
|
||||
}
|
||||
|
||||
public static LibraryExport from(Library library) {
|
||||
var aliasList =
|
||||
Objects.nonNull(library.getAliases())
|
||||
? library.getAliases().stream().map(a -> a.getAlias()).toList()
|
||||
: List.<String>of();
|
||||
|
||||
var symbolList =
|
||||
Objects.nonNull(library.getSymbols())
|
||||
? library.getSymbols().stream().map(SymbolEntry::from).toList()
|
||||
: List.<SymbolEntry>of();
|
||||
|
||||
return new LibraryExport(library.getName(), library.getDescription(), aliasList, symbolList);
|
||||
}
|
||||
}
|
||||
@ -39,4 +39,38 @@ public interface MutationSuggestionRepository extends JpaRepository<MutationSugg
|
||||
""")
|
||||
List<MutationSuggestion> findApprovedFunctionRenamesByAnalysisId(
|
||||
@Param("analysisId") UUID analysisId);
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT m FROM MutationSuggestion m
|
||||
JOIN StaticLabel l ON l.id = m.targetId
|
||||
JOIN StaticFunction f ON f.id = l.function.id
|
||||
JOIN StaticAnalysis a ON a.id = f.analysis.id
|
||||
WHERE m.targetType = 'LABEL' AND m.fieldName = 'name'
|
||||
AND a.id = :analysisId AND m.status = 'APPROVED'
|
||||
""")
|
||||
List<MutationSuggestion> findApprovedLabelRenamesByAnalysisId(
|
||||
@Param("analysisId") UUID analysisId);
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT m FROM MutationSuggestion m
|
||||
JOIN StaticDataLabel l ON l.id = m.targetId
|
||||
JOIN StaticAnalysis a ON a.id = l.analysis.id
|
||||
WHERE m.targetType = 'GLOBAL_VAR' AND m.fieldName = 'name'
|
||||
AND a.id = :analysisId AND m.status = 'APPROVED'
|
||||
""")
|
||||
List<MutationSuggestion> findApprovedGlobalVarRenamesByAnalysisId(
|
||||
@Param("analysisId") UUID analysisId);
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT m FROM MutationSuggestion m
|
||||
JOIN StaticStruct s ON s.id = m.targetId
|
||||
JOIN StaticAnalysis a ON a.id = s.analysis.id
|
||||
WHERE m.targetType = 'STRUCT' AND m.fieldName = 'name'
|
||||
AND a.id = :analysisId AND m.status = 'APPROVED'
|
||||
""")
|
||||
List<MutationSuggestion> findApprovedStructRenamesByAnalysisId(
|
||||
@Param("analysisId") UUID analysisId);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package ai.decompile.analysis.service;
|
||||
|
||||
import ai.decompile.analysis.model.dto.LibraryExport;
|
||||
import ai.decompile.analysis.model.dto.LibraryResponse;
|
||||
import ai.decompile.analysis.model.dto.LibrarySymbolResponse;
|
||||
import ai.decompile.analysis.model.entity.Library;
|
||||
@ -35,6 +36,7 @@ public class LibraryService {
|
||||
private final LibraryAliasRepository aliasRepository;
|
||||
private final LibrarySymbolRepository symbolRepository;
|
||||
private final LibrarySymbolParserRegistry parserRegistry;
|
||||
private final SymbolResolutionService symbolResolutionService;
|
||||
|
||||
@Transactional
|
||||
public LibraryResponse parseAndStore(
|
||||
@ -85,6 +87,64 @@ public class LibraryService {
|
||||
return LibraryResponse.from(library);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public LibraryResponse importFromExport(LibraryExport data) {
|
||||
var name = data.name();
|
||||
if (Objects.isNull(name) || name.isBlank()) {
|
||||
throw new IllegalArgumentException("Library name is required");
|
||||
}
|
||||
|
||||
var library =
|
||||
libraryRepository.save(
|
||||
Library.builder().name(name).description(data.description()).build());
|
||||
|
||||
var aliasList = data.aliases();
|
||||
if (Objects.nonNull(aliasList)) {
|
||||
for (var alias : aliasList) {
|
||||
aliasRepository.save(LibraryAlias.builder().library(library).alias(alias).build());
|
||||
}
|
||||
}
|
||||
|
||||
var symbolList = data.symbols();
|
||||
if (Objects.nonNull(symbolList) && !symbolList.isEmpty()) {
|
||||
var batch = new ArrayList<LibrarySymbol>(SYMBOL_BATCH_SIZE);
|
||||
|
||||
for (var entry : symbolList) {
|
||||
batch.add(
|
||||
LibrarySymbol.builder()
|
||||
.library(library)
|
||||
.moduleName(entry.moduleName())
|
||||
.className(entry.className())
|
||||
.symbolName(entry.symbolName())
|
||||
.ordinal(entry.ordinal())
|
||||
.symbolType(entry.symbolType())
|
||||
.parameters(entry.parameters())
|
||||
.category(entry.category())
|
||||
.build());
|
||||
|
||||
if (batch.size() >= SYMBOL_BATCH_SIZE) {
|
||||
symbolRepository.saveAll(batch);
|
||||
batch.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (!batch.isEmpty()) {
|
||||
symbolRepository.saveAll(batch);
|
||||
}
|
||||
}
|
||||
|
||||
var aliasCount = Objects.nonNull(aliasList) ? aliasList.size() : 0;
|
||||
var symbolCount = Objects.nonNull(symbolList) ? symbolList.size() : 0;
|
||||
log.info("Library '{}' imported with {} symbols and {} aliases", name, symbolCount, aliasCount);
|
||||
|
||||
var resolved = symbolResolutionService.resolveAllUnresolved();
|
||||
if (resolved > 0) {
|
||||
log.info("Import of '{}' resolved {} previously unresolved imports", name, resolved);
|
||||
}
|
||||
|
||||
return LibraryResponse.from(library);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<LibraryResponse> listLibraries() {
|
||||
return libraryRepository.findAll().stream().map(LibraryResponse::from).toList();
|
||||
@ -109,6 +169,13 @@ public class LibraryService {
|
||||
.map(LibrarySymbolResponse::from);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public LibraryExport exportLibrary(UUID id) {
|
||||
var library = findLibrary(id);
|
||||
|
||||
return LibraryExport.from(library);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public LibrarySymbolResponse getSymbol(UUID id) {
|
||||
return LibrarySymbolResponse.from(findSymbol(id));
|
||||
|
||||
@ -175,8 +175,7 @@ public class MutationService {
|
||||
var fieldName = "comments";
|
||||
|
||||
var oldValue = handler.getCurrentValue(functionId, fieldName);
|
||||
handler.applyComment(functionId, comment, address);
|
||||
var newValue = handler.getCurrentValue(functionId, fieldName);
|
||||
var newValue = handler.computeUpdatedComments(oldValue, comment, address);
|
||||
|
||||
var mutation =
|
||||
buildMutation(targetType, functionId, fieldName, oldValue, newValue, source, "PENDING");
|
||||
@ -285,10 +284,30 @@ public class MutationService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, String> buildRenameMap(UUID analysisId) {
|
||||
return mutationRepository.findApprovedFunctionRenamesByAnalysisId(analysisId).stream()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
MutationSuggestion::getOldValue, MutationSuggestion::getNewValue, (_, b) -> b));
|
||||
var functionRenames = mutationRepository.findApprovedFunctionRenamesByAnalysisId(analysisId);
|
||||
var labelRenames = mutationRepository.findApprovedLabelRenamesByAnalysisId(analysisId);
|
||||
var globalVarRenames = mutationRepository.findApprovedGlobalVarRenamesByAnalysisId(analysisId);
|
||||
var structRenames = mutationRepository.findApprovedStructRenamesByAnalysisId(analysisId);
|
||||
|
||||
var map = new java.util.HashMap<String, String>();
|
||||
|
||||
for (var m : labelRenames) {
|
||||
map.put(m.getOldValue(), m.getNewValue());
|
||||
}
|
||||
|
||||
for (var m : globalVarRenames) {
|
||||
map.put(m.getOldValue(), m.getNewValue());
|
||||
}
|
||||
|
||||
for (var m : structRenames) {
|
||||
map.put(m.getOldValue(), m.getNewValue());
|
||||
}
|
||||
|
||||
for (var m : functionRenames) {
|
||||
map.put(m.getOldValue(), m.getNewValue());
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
||||
@ -52,7 +52,13 @@ public class FunctionTargetHandler implements MutationTargetHandler {
|
||||
switch (fieldName) {
|
||||
case "name" -> func.setName(newValue);
|
||||
case "signature" -> func.setSignature(newValue);
|
||||
case "comments" -> func.setComments(appendComment(func.getComments(), newValue, ""));
|
||||
case "comments" -> {
|
||||
if ("SET".equals(operation)) {
|
||||
func.setComments(newValue);
|
||||
} else {
|
||||
func.setComments(appendComment(func.getComments(), newValue, ""));
|
||||
}
|
||||
}
|
||||
case "flags" -> func.setFlags(applyFlagOperation(func.getFlags(), newValue, operation));
|
||||
default -> throw new IllegalArgumentException("Unknown field: " + fieldName);
|
||||
}
|
||||
@ -72,7 +78,13 @@ public class FunctionTargetHandler implements MutationTargetHandler {
|
||||
functionRepository.save(func);
|
||||
}
|
||||
|
||||
public String computeUpdatedComments(String existingComments, String comment, String address) {
|
||||
return appendComment(existingComments, comment, address);
|
||||
}
|
||||
|
||||
private String appendComment(String existingComments, String newCommentText, String address) {
|
||||
validateNotJsonArray(newCommentText);
|
||||
|
||||
var list = new ArrayList<Map<String, Object>>();
|
||||
|
||||
if (Objects.nonNull(existingComments)
|
||||
@ -104,6 +116,14 @@ public class FunctionTargetHandler implements MutationTargetHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNotJsonArray(String commentText) {
|
||||
var trimmed = commentText.trim();
|
||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||
throw new IllegalArgumentException(
|
||||
"Comment text looks like a JSON array. Pass only the comment text, not the full comments array.");
|
||||
}
|
||||
}
|
||||
|
||||
private String applyFlagOperation(String currentFlags, String flag, String operation) {
|
||||
var parts = new ArrayList<String>();
|
||||
if (Objects.nonNull(currentFlags) && !currentFlags.isBlank()) {
|
||||
|
||||
@ -3,11 +3,13 @@ package ai.decompile.job.service;
|
||||
import ai.decompile.ai.service.EmbeddingService;
|
||||
import ai.decompile.analysis.event.MutationAppliedEvent;
|
||||
import ai.decompile.analysis.event.StaticAnalysisCompletedEvent;
|
||||
import ai.decompile.analysis.model.repository.StaticLabelRepository;
|
||||
import ai.decompile.job.messaging.JobMessagePublisher;
|
||||
import ai.decompile.job.model.entity.Job;
|
||||
import ai.decompile.job.model.enums.JobStatus;
|
||||
import ai.decompile.job.model.enums.JobType;
|
||||
import ai.decompile.job.model.repository.JobRepository;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@ -24,6 +26,7 @@ public class EmbeddingEventSubscriber {
|
||||
private final JobRepository jobRepository;
|
||||
private final JobMessagePublisher jobMessagePublisher;
|
||||
private final EmbeddingService embeddingService;
|
||||
private final StaticLabelRepository labelRepository;
|
||||
|
||||
@ApplicationModuleListener
|
||||
public void on(StaticAnalysisCompletedEvent event) {
|
||||
@ -48,8 +51,25 @@ public class EmbeddingEventSubscriber {
|
||||
event.fieldName(),
|
||||
event.targetId());
|
||||
|
||||
if ("FUNCTION".equals(event.targetType()) && "name".equals(event.fieldName())) {
|
||||
embeddingService.reindexFunction(event.targetId(), event.binaryId());
|
||||
if (!"name".equals(event.fieldName())) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.targetType()) {
|
||||
case "FUNCTION":
|
||||
embeddingService.reindexFunction(event.targetId(), event.binaryId());
|
||||
break;
|
||||
case "LABEL":
|
||||
var label = labelRepository.findById(event.targetId()).orElse(null);
|
||||
if (Objects.nonNull(label)) {
|
||||
embeddingService.reindexFunction(label.getFunction().getId(), event.binaryId());
|
||||
}
|
||||
break;
|
||||
case "GLOBAL_VAR", "STRUCT":
|
||||
embeddingService.reindexAllFunctions(event.binaryId());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -29,13 +29,18 @@ spring:
|
||||
base-url: http://localhost:11434
|
||||
embedding:
|
||||
options:
|
||||
model: ${AI_EMBEDDING_MODEL:qwen3-embedding:latest}
|
||||
model: ${AI_EMBEDDING_MODEL:nomic-embed-text}
|
||||
chat:
|
||||
options:
|
||||
model: ${AI_CHAT_MODEL_OLLAMA:gemma4:12b}
|
||||
temperature: 0.3
|
||||
top-p: 0.95
|
||||
top-k: 64
|
||||
openai:
|
||||
base-url: ${AI_CLOUD_BASE_URL:http://localhost:11434/v1}
|
||||
chat:
|
||||
options:
|
||||
model: ${AI_CLOUD_CHAT_MODEL:gemma4:12b}
|
||||
|
||||
app:
|
||||
storage:
|
||||
@ -63,7 +68,7 @@ app:
|
||||
ai:
|
||||
default-chat-provider: ${AI_DEFAULT_CHAT_PROVIDER:ollama}
|
||||
embedding:
|
||||
dimension: ${AI_EMBEDDING_DIMENSION:4096}
|
||||
dimension: ${AI_EMBEDDING_DIMENSION:768}
|
||||
chunk-max-chars: 3000
|
||||
batch-size: 20
|
||||
rag:
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE embedding_chunk
|
||||
ALTER COLUMN embedding TYPE vector(768);
|
||||
@ -23,6 +23,7 @@ spring:
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration
|
||||
- org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration
|
||||
|
||||
app:
|
||||
storage:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user