+ * This class isolates the complexity of threading, session caching, and error handling + * from the presentation layer. + *
+ */ +public class AIChatCoordinator { + + private final AIBackendService backendService; + private final AIWhatsappChatExtractor extractor; + + // Track lists to support both single and multi-chat + private ListThis client supports two primary operations:
+ *It uses Java's {@link HttpClient} for network communication and {@link Gson} + * for JSON serialization/deserialization.
+ * + *Thread safety: This class is thread-safe provided that the supplied + * {@link AIBackendConfig} is immutable.
+ */ +public class AIBackendClient implements AIBackendService { + + private static final String THINKING_BLOCK_PREFIX = "[[AI_THINKING]]"; + private static final String THINKING_BLOCK_SUFFIX = "[[/AI_THINKING]]"; + + private final AIBackendConfig config; + private final HttpClient httpClient; + private final Gson gson; + + /** + * Constructs a new {@code AIBackendClient}. + * + * @param config Backend configuration (must not be null). + * @throws IllegalArgumentException if config is null + */ + public AIBackendClient(AIBackendConfig config) { + this.config = config; + this.gson = new Gson(); + // Create an HTTP client explicitly locked to HTTP/1.1 (required for the backend) + this.httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + } + + /** + * Initializes a new chat session with the backend. + * + *This method sends the initial chat content (HTML formatted) to the backend, + * which responds with a chat identifier
+ * + * @param chatHtml Initial chat content in HTML format + * @return The MD5 chat_hash returned by the server. + * @throws AIBackendException if: + *This method sends a user question and listens for incremental responses + * from the backend. Each chunk of data is processed and forwarded to the UI + * via the provided {@link Consumer}.
+ * + * @param chatHash Unique identifier of the chat session + * @param question User's input question + * @param history The previous messages of this same chat + * @param eventHandler Callback invoked for each streamed content chunk + * @throws AIBackendException if: + *+ * This method converts the structured DTO into JSON and parses the resulting + * JSON array response back into a standard Java List of strings + *
+ * @param requestPayload The initialized multi-chat configuration + * @return A list of unique hashes representing the cached sessions on the backend + * @throws AIBackendException if the network fails or the backend returns an error JSON object + */ + @Override + public List+ * This method holds an open HTTP/1.1 InputStream and processes incoming + * {@code data: {JSON}} strings line-by-line. It parses out standard text tokens, + * formats reasoning/status tags into italics, and delegates the result to the UI + *
+ * * @param chatHashes The list of target session hashes + * @param question The prompt + * @param history Conversational memory + * @param eventHandler The UI consumption callback + * @throws AIBackendException if the HTTP stream drops or reports a backend error event + */ + @Override + public void streamMultiChatResponse(List+ * Unlike the summarized multi-chat flow, this endpoint sends complete chat + * contents to the backend. The backend processes each chat independently and + * returns an array containing either a generated hash or an error for each item. + *
+ * + * @param requestPayload The full multi-chat initialization payload containing + * raw chat HTML content + * @return A list of chat hashes successfully initialized by the backend + * @throws AIBackendException if the request fails or no chats can be initialized + */ + @Override + public List+ * The request payload is identical to the summarized multi-chat flow. The only + * difference is the backend endpoint, which operates on sessions initialized + * with raw HTML chat content. + *
+ * + * @param chatHashes Hashes returned during full multi-chat initialization + * @param question User question + * @param history Previous conversation messages + * @param eventHandler Callback that receives streamed content chunks + * @throws AIBackendException if the request or stream fails + */ + @Override + public void streamMultiChatFullResponse(List+ * This class encapsulates the endpoint URL and authentication credentials. + * By passing this object to the backend services rather than individual string + * arguments, the system remains flexible and easier to maintain. + *
+ */ +public class AIBackendConfig { + + // The root URL of the AI API endpoint + private final String baseUrl; + + // The authentication key required to access the AI service. + private final String apiKey; + + public AIBackendConfig(String baseUrl, String apiKey) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + } + + public String getBaseUrl() { + return baseUrl; + } + + public String getApiKey() { + return apiKey; + } + + /** + * Command-Line Configuration (Current) + *+ * Currently, this loads the configuration from Java System Properties + * (looks for terminal flags passed when IPED starts up) + *
+ *+ * If no flags are provided, it safely defaults to a local developer setup. + *
+ * TODO: IPED Global Configuration Integration + *This method should eventually be replaced or updated to read directly + * from IPED's configuration files
+ * @return A fully initialized AIBackendConfig. + */ + public static AIBackendConfig loadFromSystemProperties() { + // Default to local SARD backend port 8000 + String url = System.getProperty("iped.ai.url", "http://localhost:8000"); + String apiKey = System.getProperty("iped.ai.key", "change-me"); + return new AIBackendConfig(url, apiKey); + } +} \ No newline at end of file diff --git a/iped-app/src/main/java/iped/app/ui/ai/backend/AIBackendException.java b/iped-app/src/main/java/iped/app/ui/ai/backend/AIBackendException.java new file mode 100644 index 0000000000..58f7b321c3 --- /dev/null +++ b/iped-app/src/main/java/iped/app/ui/ai/backend/AIBackendException.java @@ -0,0 +1,37 @@ +package iped.app.ui.ai.backend; + +/** + * A custom checked exception representing errors that occur within the AI backend layer. + *+ * This exception serves as a generic wrapper for various lower-level failures that might + * happen during AI processing, such as network timeouts when reaching an LLM API, + * JSON parsing errors, or authentication failures. By catching low-level exceptions + * and wrapping them in an {@code AIBackendException}, the UI and presentation layers + * can handle AI-specific errors gracefully without needing to know the implementation + * details of the backend. + *
+ */ +public class AIBackendException extends Exception { + + /** + * Constructs a new AIBackendException with the specified detail message. + * * @param message The detail message explaining the specific failure (e.g., "Connection to LLM timed out"). + */ + public AIBackendException(String message) { + super(message); + } + + /** + * Constructs a new AIBackendException with the specified detail message and root cause. + *+ * This constructor is used for "Exception Chaining," allowing you to preserve the original + * stack trace of the underlying error (like an IOException) while presenting a clean + * AIBackendException to the rest of the application. + *
+ * @param message The detail message explaining the specific failure. + * @param cause The underlying cause of the failure (can be null). + */ + public AIBackendException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/iped-app/src/main/java/iped/app/ui/ai/backend/AIBackendService.java b/iped-app/src/main/java/iped/app/ui/ai/backend/AIBackendService.java new file mode 100644 index 0000000000..08155782d0 --- /dev/null +++ b/iped-app/src/main/java/iped/app/ui/ai/backend/AIBackendService.java @@ -0,0 +1,71 @@ +package iped.app.ui.ai.backend; + +import java.util.function.Consumer; +import java.util.List; + +public interface AIBackendService { + + // --- SINGLE-CHAT ENDPOINTS --- + + /** + * Sends the extracted HTML content to the backend to initialize the chat. + * @param chatHtml The raw WhatsApp HTML string. + * @return The MD5 chat_hash returned by the server. + * @throws AIBackendException if the server rejects the HTML or is unreachable. + */ + String initChat(String chatHtml) throws AIBackendException; + + /** + * Queries the LLM regarding a previously initialized chat. + * @param chatHash The hash returned from initChat. + * @param question The user's question. + * @param history The chat history composed of previous messages + * @param eventHandler A callback that receives streamed tokens/status updates from the LLM. + * @throws AIBackendException if the streaming connection fails or returns an error token. + */ + void streamChatResponse(String chatHash, String question, List+ * This method avoids LLM token-limit errors by explicitly refusing raw HTML and + * relying only on condensed metadata and summaries + *
+ * @param request The structured payload containing the batch of summaries + * @return A list of successfully cached session hashes, one for each summary provided + * @throws AIBackendException if the backend rejects the payload format or is unreachable + */ + List+ * This class encapsulates the context data that needs to be uploaded to the LLM to start + * a new stateful chat session. + *
+ */ +public class AIInitChatRequest { + + /** + * The raw text or HTML content to be analyzed by the AI. + * Note: The snake_case naming convention is used intentionally here to map + * directly to the Python backend's expected JSON schema + */ + private final String chat_content; + + public AIInitChatRequest(String chat_content) { + this.chat_content = chat_content; + } + + public String getChatContent() { + return chat_content; + } +} diff --git a/iped-app/src/main/java/iped/app/ui/ai/backend/AIInitMultiChatFullRequest.java b/iped-app/src/main/java/iped/app/ui/ai/backend/AIInitMultiChatFullRequest.java new file mode 100644 index 0000000000..6643e448c5 --- /dev/null +++ b/iped-app/src/main/java/iped/app/ui/ai/backend/AIInitMultiChatFullRequest.java @@ -0,0 +1,26 @@ +package iped.app.ui.ai.backend; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * DTO for initializing a full, non-summarized multi-chat session. + * Expects a list of raw WhatsApp HTML export strings. + */ +public class AIInitMultiChatFullRequest { + + @SerializedName("chat_contents") + private List+ * Unlike a standard single chat that uploads raw HTML, the Multi-Chat endpoint + * expects a curated list of pre-computed summaries. This class maps exactly to + * the JSON schema expected by the backend + *
+ */ +public class AIInitMultiChatRequest { + + /** + * The list of chats to be analyzed together. + * Note: The naming convention is used intentionally here to map + * directly to the backend's expected JSON schema + */ + private final List+ * Note that SummarizedChat refers to a single chat within summarized_chats. As such, + * each has only one summary and one summary id. Therefore, when constructing a SummarizedChat + * entity, consider that the backend requires a list of summaries and summaryIds, even if + * in reality the chat has only one of each + *
+ */ + public static class SummarizedChat { + private final String chat_id; + private final String chat_name; + private final List+ * Maps to the backend's MultiChatConversationRequest schema + *
+ */ +public class AIMultiChatStreamRequest { + + /** + * {@code chats_hashes} is the correct name in the Python schema, despite the unnecessary 's'. + * Do not rename this to {@code chat_hashes} + */ + private final List+ * Sent to /api/chat/stream to query a previously initialized chat + *
+ */ +public class AIStreamChatRequest { + + /** + * Note: The naming convention is used intentionally here to map + * directly to the Python backend's expected JSON schema + */ + private final String chat_hash; + private final String user_question; + private final List+ * This class provides thread-safe operations for adding, removing, and clearing + * context files used by the AI. It also implements an event-listener mechanism + * to notify interested components whenever the context changes. + *
+ * + *+ * Internally, it uses a {@link CopyOnWriteArrayList} to ensure safe concurrent access + * without explicit synchronization for read-heavy operations. + *
+ * + *+ * All UI-related notifications are dispatched on the Swing Event Dispatch Thread (EDT). + *
+ */ +public class AIContextManager { + + /** Singleton instance */ + private static AIContextManager instance; + + /** Thread-safe list holding context files */ + private final List+ * Ensures that notifications are dispatched on the Swing EDT. + *
+ * + * @param changeType the type of change that occurred + */ + private void fireContextChanged(String changeType) { + ContextChangeEvent event = new ContextChangeEvent(this, changeType); + Runnable notification = () -> { + for (ContextChangeListener listener : listeners.getListeners(ContextChangeListener.class)) { + listener.contextChanged(event); + } + }; + + if (SwingUtilities.isEventDispatchThread()) { + notification.run(); + } else { + SwingUtilities.invokeLater(notification); + } + } + + /** + * Batch adds multiple files to the context. + *+ * This method avoids UI thread starvation by processing all additions first + * and firing a single change event at the end. + *
+ * + * @param items list of items to add; ignored if {@code null} or empty + */ + public void addContextFiles(List+ * This event is fired whenever a context file is added, removed, or when + * the entire context is cleared. It provides the type of change via + * {@link #getChangeType()}. + *
+ */ +public class ContextChangeEvent extends EventObject { + + /** Change type constant for adding a file */ + public static final String FILE_ADDED = "fileAdded"; + + /** Change type constant for removing a file */ + public static final String FILE_REMOVED = "fileRemoved"; + + /** Change type constant for clearing all files */ + public static final String CLEARED = "cleared"; + + /** The type of change that occurred */ + private final String changeType; + + /** + * Constructs a new {@code ContextChangeEvent}. + * + * @param source the object that originated the event + * @param changeType the type of change (one of the static constants) + */ + public ContextChangeEvent(Object source, String changeType) { + super(source); + this.changeType = changeType; + } + + /** + * Returns the type of change that occurred. + * + * @return the change type string + */ + public String getChangeType() { + return changeType; + } +} diff --git a/iped-app/src/main/java/iped/app/ui/ai/context/ContextChangeListener.java b/iped-app/src/main/java/iped/app/ui/ai/context/ContextChangeListener.java new file mode 100644 index 0000000000..4f36dcac30 --- /dev/null +++ b/iped-app/src/main/java/iped/app/ui/ai/context/ContextChangeListener.java @@ -0,0 +1,21 @@ +package iped.app.ui.ai.context; + +import java.util.EventListener; + +/** + * Listener interface for receiving AI context change events. + *+ * Implementations of this interface can be registered with + * {@link AIContextManager} to receive notifications whenever the + * context files are added, removed, or cleared. + *
+ */ +public interface ContextChangeListener extends EventListener { + + /** + * Invoked when the AI context changes. + * + * @param event the context change event containing the change type + */ + void contextChanged(ContextChangeEvent event); +} diff --git a/iped-app/src/main/java/iped/app/ui/ai/context/ConversationManager.java b/iped-app/src/main/java/iped/app/ui/ai/context/ConversationManager.java new file mode 100644 index 0000000000..489e7b5b0f --- /dev/null +++ b/iped-app/src/main/java/iped/app/ui/ai/context/ConversationManager.java @@ -0,0 +1,131 @@ +package iped.app.ui.ai.context; + +import iped.app.ui.ai.model.AIChatMessage; +import iped.app.ui.ai.model.Conversation; +import iped.app.ui.ai.model.StandardConversation; +import iped.app.ui.ai.model.AgentConversation; +import iped.app.ui.ai.util.ConversationPersistence; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Singleton manager responsible for maintaining the state of AI conversations + * for the currently active IPED case. + */ +public class ConversationManager { + + private static ConversationManager instance; + + private final List+ * This method enforces the "context-edit lock" behavior: + *
Internal developer documentation — IPED AI Agent Module — July 2026
+ + + + +The Agent is an AI assistant integrated into IPED that communicates with an external opencode CLI process via JSON-Lines over stdin/stdout. It has autonomous access to the IPED case through a MCP (Model Context Protocol) server that exposes case data as callable tools.
The agent differs fundamentally from the chatbot in that:
+opencode processopencode.exe, which manages its own LLM provider connections, context, and conversation state. IPED acts as the orchestrator, launching the process and parsing its JSON-Lines output.
+The agent module is organized in five layers. The Java UI layer routes
+user input to OpenCodeAgentService, which spawns and
+communicates with the external opencode process. opencode
+delegates IPED-specific work to the MCP server, which calls back into
+the Java API through PyJnius.
| Layer | +Component | +File | +Responsibility | +
|---|---|---|---|
| View | +SidebarPanel | +SidebarPanel.java |
+ "New Agent Chat" dropdown, "(Agent)" label | +
| AIChatPanel | +AIChatPanel.java |
+ Shared chat panel (same as chatbot) | +|
| Controller | +AIAssistantController | +AIAssistantController.java |
+ Routes to agent via isAgentConversation() |
+
| AIChatCoordinator | +AIChatCoordinator.java |
+ Dispatches via askAgentQuestion() |
+ |
| Agent Service | +OpenCodeAgentService | +OpenCodeAgentService.java |
+ Process lifecycle, JSON-Lines parsing, stdin/stdout | +
| Model | +AgentConversation | +AgentConversation.java |
+ Extends Conversation with sessionId |
+
| ConversationManager | +ConversationManager.java |
+ startNewConversation(true) creates agent conv. |
+ |
| ConversationPersistence | +ConversationPersistence.java |
+ agent_ prefix, persists sessionId |
+ |
| External | +opencode CLI | +opencode.exe |
+ LLM provider, MCP tool execution | +
| MCP Server | +main.py | +src/main.py |
+ Entry point, stdio transport, stdout redirect | +
| config.py | +src/config.py |
+ Settings, path resolution | +|
| jvm_bridge.py | +src/jvm_bridge.py |
+ PyJnius JVM initialization | +|
| case_manager.py | +src/case_manager.py |
+ IPEDCaseManager (search, items) | +
The core service class that manages the opencode process lifecycle and handles all communication.
iped-app/src/main/java/iped/app/ui/ai/agent/OpenCodeAgentService.java
+| Field | Type | Purpose |
|---|---|---|
process | Process | Handle to the running opencode process |
processWriter | BufferedWriter | Writes commands to opencode's stdin |
readerThread | Thread | Background thread reading stdout JSON-Lines |
chatHistories | Map<Integer, List<Message>> | Local chat history per conversation (in-memory only) |
activeCallbacks | Map<Integer, MessageCallback> | Tracks active callbacks for streaming responses |
| Method | Purpose |
|---|---|
askAgentQuestion(text, convId, callback) | Main entry point. Spawns process if needed, sends message, starts reader |
spawn() | Launches opencode.exe as a subprocess with configured arguments |
sendCommand(command) | Writes a JSON command to the process stdin |
readResponse(convId) | Reads JSON-Lines from stdout until a complete response is assembled |
parseJsonLine(line) | Parses a single JSON-Lines entry, extracting text/tool usage |
extractSessionId(line) | Extracts the session ID from opencode's initial response |
stopAgent() | Sends termination command and destroys the process |
iped-app/src/main/java/iped/app/ui/ai/model/AgentConversation.java
+Extends Conversation with a single additional field:
+public class AgentConversation extends Conversation {
+ private String sessionId; // opencode session ID for resuming conversations
+ // getter/setter
+}
+
+The sessionId is extracted from the opencode process output and used to maintain conversation context across multiple questions within the same chat session.
In AIAssistantController.java, the method isAgentConversation() checks if the current conversation is an AgentConversation instance. When true:
AIChatCoordinator.askAgentQuestion() is called instead of the chatbot pathAgentConversationManager.getAgentConversations() returns only agent-type conversationsopencode is a command-line AI coding assistant that:
opencode.json)OpenCodeAgentService.spawn() executes:
+opencode.exe --non-interactive --cwd <case-dir> ++
Key launch details:
+iped-app/resources/scripts/mcp/iped-mcp-server/opencode.json
+This configuration file is placed alongside the MCP server and tells opencode:
+The MCP server is a Python process that runs inside the opencode process space. It communicates with opencode via stdio (stdin/stdout JSON-RPC) and with IPED's Java API via PyJnius (Python-Java bridge).
+ +iped-app/resources/scripts/mcp/iped-mcp-server/src/main.py
+The entry point performs three critical tasks:
+sys.stdout is re-assigned to sys.stderr. This is essential because MCP uses stdin/stdout for JSON-RPC protocol, and any stray print statements would corrupt the protocol. All logging goes to stderr instead.jvm_bridge.init_jvm() to start the Java Virtual Machine via PyJnius
+import sys
+sys.stdout = sys.stderr # Critical: redirect stdout for MCP protocol
+
+from config import Settings
+from jvm_bridge import init_jvm
+from case_manager import IPEDCaseManager
+# ... tool imports ...
+
+settings = Settings()
+init_jvm(settings)
+
+server = FastMCP("IPED MCP Server")
+
+@server.tool()
+def search(query: str, ...): ...
+
+# ... register all tools ...
+
+if __name__ == "__main__":
+ server.run()
+
+
+iped-app/resources/scripts/mcp/iped-mcp-server/src/config.py
+The Settings dataclass manages all configuration:
| Field | Default | Purpose |
|---|---|---|
iped_jar_path | auto-detected | Path to iped.jar |
iped_lib_dir | auto-detected | Path to lib/ directory for classpath |
case_path | from env or CWD | Path to the IPED case directory |
temp_dir | temp/iped-mcp | Temporary files directory |
max_results | 100 | Default max search results |
jvm_args | -Xmx2g | JVM heap configuration |
Path resolution follows a priority chain: environment variable → relative to script location → working directory fallback.
+The opencode configuration registers the IPED MCP server:
+
+{
+ "mcpServers": {
+ "iped": {
+ "command": "python",
+ "args": ["src/main.py"],
+ "cwd": "iped-app/resources/scripts/mcp/iped-mcp-server",
+ "env": {
+ "IPED_CASE_PATH": "${IPED_CASE_PATH}"
+ }
+ }
+ }
+}
+
+
+IPED_CASE_PATH is injected by OpenCodeAgentService when spawning the process, so the MCP server knows which case to open.
+Tools are declared via Python decorators in the MCP server. opencode reads these declarations at startup and makes the tools available to the LLM. The LLM then decides when to call each tool based on the user's request.
+Available tools:
+| Category | Tools |
|---|---|
| Search | search, search_by_type, search_by_name, get_searchable_fields |
| Documents | get_document, get_document_content, get_document_text |
| Bookmarks | list_bookmarks, get_bookmark |
| Sources | list_sources |
iped-app/resources/scripts/mcp/iped-mcp-server/src/jvm_bridge.py
+The JVM bridge uses PyJnius (a Python library for accessing Java classes from CPython) to call IPED's Java API directly from Python.
+ +iped.jar and all JARs in lib/jnius.start_jvm()SearchRequestIPEDSearcherItemIdIPEDCase
+from jnius import autoclass, cast
+
+def init_jvm(settings):
+ classpath = f"{settings.iped_jar_path};{settings.iped_lib_dir}/*"
+ jnius.start_jvm(
+ classpath=classpath,
+ jvmargs=settings.jvm_args.split()
+ )
+ # Cache commonly used classes
+ global SearchRequest, IPEDSearcher, ItemId, IPEDCase
+ SearchRequest = autoclass("iped.search.SearchRequest")
+ IPEDSearcher = autoclass("iped.search.IPEDSearcher")
+ ItemId = autoclass("iped.data.ItemId")
+ IPEDCase = autoclass("iped.config.IPEDCase")
+
+
+iped-app/resources/scripts/mcp/iped-mcp-server/src/case_manager.py
+The IPEDCaseManager is a Python class that wraps the IPED Java API calls, providing a clean Python interface for the MCP tools.
| Method | Purpose |
|---|---|
open_case(case_path) | Opens an IPED case and initializes the case object |
search(query) | Executes a Lucene query, returns list of matching items |
get_item_by_id(item_id) | Retrieves a specific item by its ID |
get_item_children(item_id) | Lists child items of a container |
get_item_text(item_id) | Extracts text content from a document |
+def search(self, query: str, max_results: int = 100):
+ search_request = SearchRequest()
+ search_request.setQuery(query)
+ search_request.setMaxResults(max_results)
+
+ searcher = IPEDSearcher(self.case, search_request)
+ results = searcher.search()
+
+ items = []
+ for item_id in results.getIds():
+ item = self.case.getItem(item_id)
+ items.append({
+ "id": str(item_id),
+ "name": item.getName(),
+ "type": item.getType(),
+ "size": item.getLength(),
+ "path": item.getPath()
+ })
+ return items
+
+
+open_case() if not already done. The case path is determined from the IPED_CASE_PATH environment variable.
+search(query, max_results=100)
+# Example LLM tool call
+search(query="*.pdf AND text:fraude")
+# Returns: [{"id": "12345", "name": "doc.pdf", "type": "application/pdf", ...}]
+
+search_by_type(mime_type, max_results=100)+search_by_type(mime_type="image/jpeg") +search_by_type(mime_type="video/mp4") ++
search_by_name(name, max_results=100)+search_by_name(name="*.xlsx") +search_by_name(name="%relatorio%") ++
get_searchable_fields()+# Returns: ["name", "ext", "type", "size", "created", "modified", +# "accessed", "path", "content", "hash", ...] ++
get_document(item_id)
+get_document(item_id="12345")
+# Returns: {id, name, type, size, path, created, modified, ...}
+
+get_document_content(item_id)+get_document_content(item_id="12345") +# Returns: "Full text content of the document..." ++
get_document_text(item_id)list_bookmarks()
+# Returns: [{"name": "Relevant", "count": 15}, {"name": "Suspicious", "count": 8}]
+
+get_bookmark(category)
+get_bookmark(category="Relevant")
+# Returns: [{id, name, type, ...}, ...]
+
+list_sources()
+# Returns: [{"name": "disk.E01", "type": "disk", "path": "/evidence/disk.E01"}]
+
+Messages sent to opencode's stdin follow this JSON structure:
+
+{
+ "command": "chat",
+ "message": "What emails mention 'confidential'?",
+ "sessionId": "abc123-def456" // null on first question
+}
+
+
+opencode outputs JSON-Lines on stdout. Each line is a separate JSON object:
+
+{"type":"text","content":"I found 5 emails mentioning "}
+{"type":"text","content":"'confidential'. Here are the results:"}
+{"type":"tool_use","name":"search","args":{"query":"confidential"}}
+{"type":"tool_result","content":"[5 results found]"}
+{"type":"text","content":"The most relevant email is..."}
+
+OpenCodeAgentService sends the question without a session ID. opencode creates a new session and returns the session ID in its initial response.extractSessionId() parses the opencode output to find the session ID. It is stored in AgentConversation.sessionId.ConversationPersistence (saved in the agent_ prefixed properties file).Despite conversations being stateless on the opencode side, OpenCodeAgentService maintains a local chat history in-memory:
+Map<Integer, List<Message>> chatHistories = new ConcurrentHashMap<>(); ++
This allows the UI to display the full conversation history even after opencode's session state is lost.
+ +| Event | Action |
|---|---|
| User asks first question | Spawn opencode process |
| User asks follow-up | Reuse existing process (if running) |
| User clicks "New Agent Chat" | Stop old process, spawn new one |
| User switches between conversations | Stop current process, spawn new one for target conversation |
| IPED shutdown | Destroy all opencode processes |
The parseJsonLine(line) method processes each line of output from opencode:
type field to determine the message type:
+ "text" — Extract content field, append to response buffer"tool_use" — Log the tool call (tool name + args), optionally display in UI"tool_result" — Log the tool result, may append summary to response"session_id" — Extract and store the session ID"error" — Handle error messagesText fragments are accumulated in a StringBuilder until the process signals completion (EOF or a terminal marker). The assembled text is then passed to the callback as the final answer.
If the opencode process crashes or exits unexpectedly:
+activeCallbacks is checked — any pending callback receives an error messagenullspawn() is called again automaticallyIf the MCP server (Python process) fails to start or crashes:
+| Error Type | Detection | Recovery |
|---|---|---|
| Invalid JSON | JSON parse exception in parseJsonLine() | Log warning, skip line, continue reading |
| Stdin write failure | IOException when writing to process | Kill process, spawn new one on next request |
| Stdin closed (process exited) | Broken pipe error | Same as crash recovery |
| Timeout | No output for extended period | Configurable timeout, kill and restart |
| Aspect | Chatbot | Agent |
|---|---|---|
| AI Model | Embedded (local inference) | External (opencode CLI → cloud/local LLM) |
| Tool Access | None — user must manually search | Autonomous MCP tools (search, bookmarks, docs) |
| Conversation State | Persistent — saved to disk, summary on overflow | Stateless — fresh process per conversation |
| History Management | Full persistence, summary extraction | In-memory only, session ID in opencode |
| Startup | Immediate (model loaded once) | Spawns opencode process on first question |
| Resource Usage | GPU/CPU for local model | Process memory + network (for cloud LLMs) |
| Independence | Relies on user to provide context via search | Can autonomously explore the case |
| Error Handling | Model inference errors, timeout | Process crashes, MCP failures, JVM issues |
| Configuration | IPED settings (model path, params) | opencode.json + MCP server config |
| Conversation ID Prefix | none | agent_ |
| File | Path |
|---|---|
OpenCodeAgentService.java | iped-app/src/main/java/iped/app/ui/ai/agent/ |
AgentConversation.java | iped-app/src/main/java/iped/app/ui/ai/model/ |
AIAssistantController.java | iped-app/src/main/java/iped/app/ui/ai/controller/ |
AIChatCoordinator.java | iped-app/src/main/java/iped/app/ui/ai/ |
SidebarPanel.java | iped-app/src/main/java/iped/app/ui/ai/view/ |
ConversationManager.java | iped-app/src/main/java/iped/app/ui/ai/context/ |
ConversationPersistence.java | iped-app/src/main/java/iped/app/ui/ai/util/ |
| File | Path |
|---|---|
main.py | iped-app/resources/scripts/mcp/iped-mcp-server/src/ |
config.py | iped-app/resources/scripts/mcp/iped-mcp-server/src/ |
jvm_bridge.py | iped-app/resources/scripts/mcp/iped-mcp-server/src/ |
case_manager.py | iped-app/resources/scripts/mcp/iped-mcp-server/src/ |
search.py | iped-app/resources/scripts/mcp/iped-mcp-server/src/tools/ |
documents.py | iped-app/resources/scripts/mcp/iped-mcp-server/src/tools/ |
bookmarks.py | iped-app/resources/scripts/mcp/iped-mcp-server/src/tools/ |
sources.py | iped-app/resources/scripts/mcp/iped-mcp-server/src/tools/ |
| File | Path |
|---|---|
opencode.json | iped-app/resources/scripts/mcp/iped-mcp-server/ |
The IPED Agent module provides a powerful AI assistant that can autonomously explore forensic cases through MCP tool integration. Unlike the chatbot (which requires the user to manually provide context), the agent can independently search for evidence, examine documents, and build understanding of the case.
+ +Key architectural decisions:
+OpenCodeAgentService.java for process management and JSON-Lines parsing, and on the Python MCP server files for tool behavior. The Java controller layer (AIAssistantController, AIChatCoordinator) handles routing and is rarely modified.
+
+ This document describes the agent feature in IPED, where an LLM-powered
+ agent (via opencode CLI) interacts with the IPED case through
+ an MCP (Model Context Protocol) server. The agent can search, read, and
+ reference forensic items using natural language.
+
+ AIAssistantPanel: top-level JFrame container. Controls + context-panel and quick-actions panel visibility (both hidden for + agent conversations). +
++ SidebarPanel: provides "New Agent Chat" dropdown + option alongside "New Chat". +
++ ChatAreaPanel: renders agent responses with + streaming. +
+
+ AIMarkdownRenderer: parses agent output markdown
+ including <<hash-chunkId>> tokens.
+
+ AIAssistantController: routes send actions to
+ AIChatCoordinator.askAgentQuestion() when the active
+ conversation is an AgentConversation.
+
+ AIChatCoordinator: extracts the session ID from the
+ active AgentConversation, creates the
+ onSessionIdFound callback, and delegates to
+ OpenCodeAgentService.
+
+ OpenCodeAgentService: spawns opencode
+ as a subprocess. Manages command construction, session ID extraction
+ from JSON output, real-time JSON-Lines parsing, and local chat
+ history.
+
+ AgentConversation: extends Conversation,
+ adds sessionId field for multi-turn opencode session
+ persistence.
+
+ opencode: standalone CLI agent. Reads
+ opencode.json config. Connects to a local LLM provider
+ (e.g. Qwen3.5-122B). Manages its own tool-calling loop. Outputs
+ JSON-Lines to stdout.
+
+ opencode.json: defines MCP server command
+ (python -m src.main), model provider, context limits.
+
+ FastMCP Server: registered as iped tool
+ provider in opencode. Runs inside the opencode process via stdio.
+
+ IPEDCaseManager: wraps Java IPED classes via PyJnius. + Opens the case, performs Lucene searches, reads item metadata/text. +
+
+ JVM Bridge: initializes JVM with IPED classpath via
+ jnius_config. Provides get_class() and
+ cast_to() helpers.
+
+ MCP Tools: search,
+ search_by_type, search_by_name,
+ get_searchable_fields, get_document,
+ get_document_content, get_document_text,
+ read, read_batch,
+ list_bookmarks, get_bookmark,
+ list_sources.
+
+ ConversationManager: manages active conversation.
+ Agent conversations use startNewConversation(true).
+
+ ConversationPersistence: saves agent conversations
+ with agent_ prefix. Persists sessionId for
+ session resumption.
+
+ AgentConversation.sessionId: opencode session ID + extracted from JSON output via regex. Enables multi-turn context. +
+
+ The user selects "New Agent Chat" from the sidebar dropdown. The
+ controller creates an AgentConversation, hides the context
+ panel (agent mode doesn't use file context), clears previous state, and
+ appends a system message.
+
+ On the first question in a new agent conversation, no session ID exists.
+ OpenCodeAgentService spawns opencode run as a
+ subprocess. The opencode process starts the MCP server, connects to the
+ LLM, and begins a tool-calling loop. The session ID is extracted from the
+ JSON-Lines output and persisted for future turns.
+
+ On subsequent questions, the sessionId is passed to
+ opencode --session <id>. This allows opencode to
+ resume the previous conversation context, including prior tool calls and
+ LLM responses. The MCP server does not re-initialize since it runs inside
+ the same opencode process.
+
+ When opencode starts, it reads opencode.json
+ and launches the MCP server as a subprocess (python -m src.main).
+ The MCP server resolves paths automatically from its own file location
+ (IPED_HOME and CASE_PATH are derived relative to
+ config.py), auto-detects JAVA_HOME via a fallback
+ chain, validates configuration, initializes the JVM with IPED classpath,
+ opens the forensic case, and registers all tools.
+
+ When the LLM decides to call a tool, opencode sends the tool invocation + to the MCP server over stdio. The MCP server executes the tool (which + may involve JVM calls via PyJnius), and returns the result. The LLM + receives the result and continues generating. +
++ Agent errors can occur at three points: opencode process fails to start, + the process exits with a non-zero code, or an exception occurs during + execution. The error handler salvages any partial draft and appends a + system error message. Unlike the chatbot flow, there is no + initialization cache to invalidate. +
+
+ Switching to a previous agent conversation restores the chat messages and
+ the sessionId. No background context restoration occurs
+ (agent conversations don't use the file context list). The coordinator
+ restores chat history for multi-turn awareness.
+
+ Deletion follows the same soft-delete pattern as standard conversations.
+ The file is saved with status = "deleted" and preserved on
+ disk. Agent files use the agent_ prefix.
+
Project Report — Branch: last-branch-documentation — July 2026
+ The chatbot feature adds a conversational AI assistant to IPED. Investigators + can select WhatsApp chat exports from a forensic case, add them to an AI + context, and ask natural-language questions. An LLM processes the chat data + and returns answers with clickable citations that navigate directly to the + referenced items in IPED's viewer. +
++ Development was structured as a progressive build-up over 157 commits, + evolving from a minimal backend integration contract into a full-featured + assistant with context management, multi-chat routing, streaming responses, + markdown rendering, collapsible thinking blocks, and persistent conversation + history. +
+ +ai/
+
+ The vast majority of the work is self-contained within the
+ iped.app.ui.ai package and its subpackages. Existing IPED code
+ was touched minimally — just enough to wire the assistant into the
+ application shell.
+
ai/)| File | +What Changed | +Why | +
|---|---|---|
App.java |
+ Added an AI Assistant toolbar button and a Ctrl+Shift+A keyboard shortcut | +Provides the entry point to launch the assistant from the main IPED window | +
MenuClass.java |
+ Added two right-click menu items: "Add all highlighted to AI context" and "Add all checked to AI context", plus helper methods to resolve items from the results table | +Enables users to send forensic items from the results table directly into the AI context | +
ViewerController.java |
+ Added a SummaryViewer to the viewer list, excluded it from auto-selection, and added cleanup logic to remove it when no summaries exist in the case index |
+ Provides a viewer tab for AI-generated summaries in the case analysis interface | +
UICaseDataLoader.java |
+ Updated import path for AIFiltersLoader (moved to filters subpackage) |
+ Package reorganization | +
UiUtil.java |
+ Added an overloaded getUIEmptyHtml(String msg) that renders a centered message on the viewer background |
+ Used by SummaryViewer to display summary content in the viewer pane |
+
ExtraProperties.java |
+ Added SUMMARY and CHUNK_IDS constant definitions |
+ Defines the metadata keys used by the summarization pipeline and the chatbot's routing logic | +
| File | What |
|---|---|
IPEDConfig.txt | Added enableAISummarization toggle (default: false) |
AISummarizationConfig.txt | New config file for summarization service address, batch sizes, and chat analysis questions |
AIFiltersConfig.json | New config for AI-specific filter categories |
TaskInstaller.xml | Registered AISummarizationTask in the task pipeline |
AISummarizationTask.py | New 677-line Python task that calls a remote summarization service and stores results in item metadata |
| Localization files (6 languages) | Added 3 keys: AIAssistant.Tooltip, AIAssistant.Title, AIAssistant.Send, plus 2 menu labels |
+ The chatbot is built as a layered architecture inside IPED. Five layers + collaborate to process user queries, from button click to rendered answer. +
+ +
+ The key insight is that the AI package is self-contained. It plugs into
+ IPED through three integration points: the toolbar button in
+ App.java, the right-click context menu in
+ MenuClass.java, and the summarization viewer in
+ ViewerController.java. Everything else lives inside the
+ ai package.
+
+ The user right-clicks one or more WhatsApp chat exports in IPED's results
+ table and selects "Add to AI context." The items are validated (must be
+ WhatsApp chats, not empty, not flagged), wrapped in ContextFileEntry
+ objects, and added to the AIContextManager's thread-safe list.
+ The context panel in the UI updates to show the selected files.
+
+ The user types a question and presses Send. The controller creates a user + message, starts a streaming animation, and delegates to the coordinator. + The coordinator runs on a background thread to avoid blocking the UI. +
+ ++ The coordinator looks at two things: how many files are in context, and how + many "chunks" they contain (a chunk is a segment the summarization pipeline + produced during case processing). Based on this, it picks one of three + backend paths: +
+ ++ This three-way split exists because sending raw HTML for many chats would + overwhelm the LLM's context window. For small contexts, full HTML gives the + best answer quality. For large contexts, summaries compress the essential + information into a manageable size. +
+ ++ The backend streams tokens back via Server-Sent Events. Each token is + forwarded from the background thread to the Swing EDT (Event Dispatch Thread), + where it enters a timer-based animation queue. Every 30 milliseconds, one + token is dequeued and appended to the draft message, which is then + re-rendered as markdown. This creates a smooth "typewriter" effect. +
+ +
+ The markdown renderer handles headings, bold, italic, blockquotes, lists,
+ and — importantly — citation tokens. Citations use the format
+ <<hash-chunkId>> and are rendered as blue
+ underlined links. Clicking one navigates IPED's viewer to the referenced
+ forensic item.
+
+ Every message is saved to disk as a JSON file in
+ <case_dir>/iped/data/ai_chats/. Conversations can be
+ switched, resumed, or deleted (soft-delete preserves the file for
+ auditability). The coordinator caches backend session hashes so follow-up
+ questions within the same context skip re-initialization.
+
+ The feature was built incrementally over 157 commits. Here is a summary + of the major phases: +
+ +| Phase | +What Was Built | +Key Commits | +
|---|---|---|
| Foundation | +Backend HTTP client, config, DTOs, WhatsApp HTML extraction, basic send flow wired to a mock backend | +feat(llm integration): add backend http clientfeat: add WhatsApp chat content extraction |
+
| Multi-turn | +Conversational memory, previous messages sent with each request, markdown rendering with draft/commit/discard lifecycle | +feat(llm): add multi turn conversational memoryfeat(markdown): enhance message handling |
+
| Multi-chat | +Multi-chat DTOs, payload factory, three backend endpoints (single, full, summarized), chunk-based routing | +feat(multi-chat-integration): implement multi-chat DTOsfeat: add multi-chat routing to coordinator |
+
| UI Refactor | +Extracted ChatAreaPanel, SidebarPanel, ContextPanel, HeaderPanel from monolithic assistant panel. Moved events through controller. | +refactor: Introduce ChatAreaPanel and HeaderPanelRefactor: make the panel components purely visual |
+
| Persistence | +Conversation model, JSON persistence, sidebar with conversation list, auto-title generation, soft-delete | +feat: define conversation data modelfeat: implement conversation persistence layer |
+
| Context Management | +Context-edit lock, auto-fork flow, validation, right-click menu integration in MenuClass | +refactor: implement context locking mechanismrefactor: Move AI context-add workflow from MenuClass to AIAssistantController |
+
| Summarization Pipeline | +Python summarization task, config files, summary viewer in IPED's viewer panel | +AISummarizationTask.py (677 lines), SummaryViewer.java |
+
| Polish | +Error recovery, partial draft salvage, thinking blocks, collapsible UI, quick actions, localization | +fix: preserve partial streaming draftfeat: implement collapsible thinking blocks |
+
+ All chatbot logic lives under iped.app.ui.ai. Existing IPED
+ code was modified in only three places: the toolbar button
+ (App.java), the context menu (MenuClass.java), and
+ the viewer panel (ViewerController.java). This keeps the
+ feature isolated and removable without affecting the rest of the application.
+
+ The AIAssistantController acts as the single entry point for
+ all events. View components (panels) are purely visual — they don't
+ contain business logic. All user actions flow through the controller, which
+ delegates to the coordinator, context manager, or conversation manager as
+ needed. This made it possible to refactor individual panels without breaking
+ the overall flow.
+
+ Rather than a single endpoint that tries to handle all data volumes, the + system uses three purpose-built backend paths. This keeps the backend simple + and deterministic: each endpoint knows exactly what format to expect and how + to process it. The trade-off is slightly more routing logic on the client + side, but the clarity is worth it. +
+ ++ Once the LLM has processed a context, it's locked in the UI. Modifying it + afterward would create an inconsistent state (the backend's cache reflects the + old context, but the UI shows new files). Instead, the system offers an + "auto-fork": a new conversation is created that inherits the old context and + merges the new files. The previous conversation remains as a historical + record. +
+ +
+ Conversations are never truly deleted from disk. They're marked with
+ status = "deleted" and hidden from the UI, but the JSON file
+ remains for forensic auditability.
+
+ The feature was built in layers: first a mock backend, then real HTTP, then + streaming, then multi-chat, then persistence, then polish. Each phase was + functional on its own, which allowed continuous testing and integration. +
+ ++ This report covers the chatbot feature only. The agent + feature (opencode CLI integration, MCP server, JVM bridge) is a separate + capability that shares some infrastructure but is not part of this report. +
+ +| In Scope (Chatbot) | Out of Scope (Agent) |
|---|---|
| WhatsApp chat context management | +OpenCodeAgentService | +
| Three-path backend routing (single / full / summarized) | +MCP server (Python, stdio transport) | +
| SSE streaming with teletype animation | +JVM bridge (PyJnius) | +
| Markdown rendering with citation tokens | +opencode CLI subprocess | +
| Context-edit lock and auto-fork | +Agent session persistence | +
| Conversation persistence and management | +Agent citation tools (get_citation_token, etc.) | +
| Error recovery and partial draft salvage | +Agent error recovery flow | +
+ Both features share the AIAssistantController,
+ AIChatCoordinator, ChatAreaPanel,
+ AIMarkdownRenderer, ChatStreamAnimator,
+ ConversationManager, ConversationPersistence, and
+ the conversation model classes. The controller routes between them based on
+ isAgentConversation().
+
| Category | Count | Details |
|---|---|---|
| New Java files | +~40 | +All under iped.app.ui.ai.* subpackages |
+
| Modified Java files | +6 | +App.java, MenuClass.java, ViewerController.java, UICaseDataLoader.java, UiUtil.java, ExtraProperties.java |
+
| Moved Java files | +5 | +AI filter classes moved from ai/ to ai/filters/ |
+
| New config/resource files | +5 | +AISummarizationConfig.txt, AIFiltersConfig.json, AISummarizationTask.py, updates to IPEDConfig.txt and TaskInstaller.xml |
+
| Localization files | +6 | +3 keys added to each of 6 languages (en, de, es, fr, it, pt) | +
| Documentation (HTML) | +4+ | +Technical reference (chatbot), 2 Sequence diagrams flow files, this report | +
+ The chatbot feature is a substantial addition to IPED, adding conversational
+ AI capabilities for forensic WhatsApp chat analysis. Despite its scope
+ (~5,800 lines of new Java code), it was designed to be minimally invasive:
+ nearly all changes live inside the new ai package, and the six
+ modified files received only small, surgical additions.
+
+ The architecture prioritizes clarity (three purpose-built backend paths over + one generic endpoint), safety (context-edit lock prevents inconsistent state), + and resilience (partial draft salvage, hash cache invalidation on backend + restarts). The progressive build-up from mock backend to full feature was + enabled by a clean separation between UI, controller, coordinator, and + network layers. +
+
+ For detailed sequence diagrams covering all 12 flows, see the companion
+ document:
+ Chatbot sequence diagrams.html. For the full technical
+ reference, see
+ Chatbot Complete Technical Documentation.html.
+
+ Internal developer documentation — IPED AI Assistant Module — July 2026 +
+ + + + + + + + + ++ The IPED AI Assistant Chatbot is a conversational interface integrated into + IPED, a digital forensics application. It allows investigators to select + WhatsApp chat exports from a forensic case and ask natural-language questions + about their content. The LLM processes the chat data and returns answers + with clickable citations that navigate directly to the referenced items in + IPED's viewer. +
++ The chatbot handles three distinct data-volume scenarios: a single chat, + multiple chats below a chunk threshold (where raw HTML is sent), and multiple + chats above the threshold (where pre-computed summaries are sent instead). + This adaptive strategy balances answer quality against LLM context-window + limits and memory consumption. +
++ Digital forensic investigations frequently involve large volumes of + messaging data. Manually reviewing WhatsApp chat exports is time-consuming + and error-prone. The chatbot addresses this by enabling investigators to + query chat content using natural language, receiving answers backed by + citations that link to the original forensic evidence. +
++ The architecture had to solve several practical constraints: +
++ These constraints drove the three-path routing strategy documented below. +
++ The chatbot follows a layered architecture with clear separation of + concerns. Five logical layers collaborate to process user queries: +
+ +AIAssistantPanel — top-level JFrame container and layout manager.
+SidebarPanel — conversation list, new-chat button, delete.
+ContextPanel — file context list with per-item removal and summarized-mode indicator.
+ChatAreaPanel — chat rendering, text input, streaming orchestration, citation click handling.
+AIMarkdownRenderer — markdown-to-StyledDocument parser, thinking blocks, citation tokens.
+ChatStreamAnimator — timer-based teletype animation engine.
+AIAssistantController — orchestrates event routing, view updates, chat lifecycle, and persistence.
+AIChatCoordinator — central pipeline: validates context, selects backend endpoint, manages session hashes, streams responses.
+AIPayloadFactory — translates ContextFileEntry lists into backend DTOs.
AIWhatsappChatExtractor — reads IPED items into UTF-8 HTML strings.
+SummaryValueExtractor — extracts pre-computed AI summaries from item metadata.
+ContextItemValidator — rejects non-WhatsApp items, empty files, empty communications.
+AIBackendClient — HTTP/SSE client implementing AIBackendService. Six endpoints.
AIBackendConfig — immutable baseUrl + apiKey.
DTOs — AIInitChatRequest, AIStreamChatRequest, AIInitMultiChatRequest, AIInitMultiChatFullRequest, AIMultiChatStreamRequest.
AIContextManager — singleton, thread-safe context file list with change-event broadcasting.
+ConversationManager — singleton, active conversation state, auto-title generation.
+ConversationPersistence — JSON serialization to <case_dir>/iped/data/ai_chats/.
+ The central controller (AIAssistantController.java) owns all
+ UI event routing. It wires the send button and Enter key to
+ handleSendAction(), manages conversation creation and deletion,
+ handles context-add events (including the auto-fork flow), and coordinates
+ persistence after every meaningful state change. It is the only class that
+ directly manipulates both the view and the coordinator.
+
+ The coordinator (AIChatCoordinator.java) is the pipeline engine.
+ It validates context entries, computes the total chunk count, decides which
+ backend endpoint to call, uploads chat data (or summaries) on a background
+ thread, streams the response back through callbacks, and maintains the
+ session hash cache so that subsequent questions skip re-initialization.
+
+ The HTTP client (AIBackendClient.java) implements the
+ AIBackendService interface and handles all network communication.
+ Initialization endpoints (/api/init_chat,
+ /api/init_multichat_with_summaries,
+ /api/init_multichat_full) return synchronously. Streaming
+ endpoints use Server-Sent Events (SSE) over HTTP/1.1, parsed by the shared
+ processSseStream() helper.
+
+ The factory (AIPayloadFactory.java) translates IPED's internal
+ ContextFileEntry objects into network-safe DTOs. It enforces a
+ strict contract: buildMultiChatRequest() requires pre-computed
+ summaries and silently skips entries that lack them;
+ buildMultiChatFullRequest() extracts raw HTML. The factory also
+ adapts IPED's flexible metadata types (String,
+ Collection, Object[]) into clean lists.
+
+ A singleton (AIContextManager.java) that maintains the list of
+ files the user has added to the current conversation's context. It uses a
+ CopyOnWriteArrayList for thread-safe read-heavy access, validates
+ items via ContextItemValidator, and fires
+ ContextChangeEvent notifications on the EDT.
+
+ A singleton (ConversationManager.java) that tracks the active
+ conversation, manages the conversation list, and auto-generates titles from
+ the first user message (truncated to 30 characters). Every
+ addMessageToActive() call triggers an asynchronous save to disk.
+
+ Handles JSON serialization to <case_dir>/iped/data/ai_chats/.
+ Chatbot conversations use the chat_ prefix; agent conversations
+ use agent_. Deletion is soft: the file is rewritten with
+ status = "deleted" but remains on disk for auditability.
+
+ The streaming subsystem comprises three classes working together: +
+startMessageStreaming(), feeds tokens via
+ enqueueStreamingToken(), drains via
+ pruneStreaming(), and salvages partial drafts on error via
+ salvageStreamingDraft().Timer
+ firing every 30ms, dequeuing one whitespace-delimited token per tick and
+ appending it to the draft message.StyledDocument on every tick, parsing markdown, thinking blocks,
+ and citation tokens.+ Context management governs how WhatsApp chat files enter the system and + how they flow through validation, extraction, and backend upload. +
+ +
+ Users add files by right-clicking items in IPED's results table and selecting
+ "Add to AI Context." The entry point is
+ AIAssistantController.addItemsToContext(), which enforces
+ several rules:
+
startNewConversationWithCurrentContext().AIContextManager.
+ Each item is validated by ContextItemValidator.getRejectionReason(),
+ which checks:
+
AIWhatsappChatExtractor.isWhatsAppChatType()).SummaryValueExtractor.hasSummary()), it is accepted immediately
+ and the remaining checks are skipped.
+ Valid items are stored as ContextFileEntry objects in the
+ AIContextManager's thread-safe list. Invalid items are stored
+ separately and shown in the context panel with their rejection reason.
+
+ Once a conversation has received an assistant reply, its context becomes + "locked" in the UI. This is a deliberate design choice: modifying the + context after the LLM has already processed it would create an inconsistent + state. The lock is enforced both in the UI (the context panel disables + editing) and in the controller (additions trigger the auto-fork flow). +
+ +
+ When a user adds files to a sealed conversation, the auto-fork flow creates
+ a new StandardConversation that inherits the previous context
+ IDs and merges the new items. The coordinator's history is cleared, and the
+ new conversation becomes the active workspace. The previous conversation
+ remains unchanged and accessible from the sidebar.
+
+ When only one WhatsApp chat file is in context, the chatbot sends the + complete HTML representation of that chat to the backend. This is the + highest-fidelity path: the LLM sees every message, every timestamp, and + every media reference. +
+ +AIWhatsappChatExtractor.extractHtml()
+ reads the IPED item's InputStream into a UTF-8 string. It
+ verifies the item is a WhatsApp chat type before reading.AIInitChatRequest ({"chat_content": "..."}) and sent
+ to /api/init_chat. The backend returns a hash that identifies
+ the cached session.currentChatHashes and persisted to the conversation object. On
+ subsequent questions with the same context, re-initialization is skipped.
+ When two or more WhatsApp chat files are in context, the system must decide
+ between two sub-paths based on the total chunk count. A "chunk" corresponds
+ to a CHUNK_IDS metadata entry on the IPED item, which the
+ backend's summarization pipeline produced during case processing.
+
validEntries.size() > 1 AND
+ totalChunks ≤ 10
++ When the combined chunk count is small (10 or fewer), the system sends the + raw HTML of every chat to the backend. This preserves full fidelity across + all conversations. +
+
+ AIPayloadFactory.buildMultiChatFullRequest() iterates over
+ valid entries, calls extractHtml() for each, and returns an
+ AIInitMultiChatFullRequest containing
+ {"chat_contents": ["html1", "html2", ...]}.
+
+ The backend endpoint is /api/init_multichat_full, which
+ processes each HTML independently and returns an array of hashes. The
+ streaming endpoint is /api/multichat_full/stream.
+
validEntries.size() > 1 AND
+ totalChunks > 10
++ When the combined chunk count exceeds 10, sending raw HTML would overwhelm + the LLM's context window. Instead, the system sends pre-computed summaries. +
+
+ AIPayloadFactory.buildMultiChatRequest() extracts summaries
+ from each entry's metadata (ExtraProperties.SUMMARY) and
+ summary IDs (ExtraProperties.CHUNK_IDS). Files without
+ summaries are silently skipped. The resulting
+ AIInitMultiChatRequest contains:
+
{
+ "summarized_chats": [
+ {
+ "chat_id": "abc123",
+ "chat_name": "WhatsApp Chat with John.txt",
+ "summaries": ["summary text chunk 1", "summary text chunk 2"],
+ "summary_ids": ["chunk_0", "chunk_1"]
+ }
+ ]
+}
+
+ The backend endpoint is /api/init_multichat_with_summaries.
+ The streaming endpoint is /api/multichat/stream.
+
ExtraProperties.SUMMARY metadata.
+ The chatbot merely reads and forwards them.
+
+ AIPayloadFactory implements a resilient fallback chain for
+ summary extraction:
+
ExtraProperties.SUMMARY from runtime
+ ExtraAttributes (in-memory).entry.getSummary(), which
+ is the UI-cached summary from the ContextFileEntry.
+ For summary IDs, if the metadata is missing or the count does not match the
+ number of summaries, synthetic IDs (summary_fallback_1,
+ summary_fallback_2, ...) are generated to maintain a 1:1
+ mapping and prevent the citation engine from crashing.
+
+ The endpoint selection is a deterministic three-way branch computed inside
+ AIChatCoordinator.askQuestion(). The decision is based on two
+ variables: the number of valid context entries and the total chunk count.
+
| Condition | +Init Endpoint | +Stream Endpoint | +Payload Factory | +
|---|---|---|---|
| 1 valid entry | +POST /api/init_chat |
+ POST /api/chat/stream |
+ Direct extractHtml() |
+
| ≥2 entries, totalChunks ≤ 10 | +POST /api/init_multichat_full |
+ POST /api/multichat_full/stream |
+ buildMultiChatFullRequest() |
+
| ≥2 entries, totalChunks > 10 | +POST /api/init_multichat_with_summaries |
+ POST /api/multichat/stream |
+ buildMultiChatRequest() |
+
+ The threshold of 10 chunks is a pragmatic balance point. Below this limit, + the total HTML content from multiple chats typically fits within the LLM's + context window without truncation. Above it, the risk of exceeding token + limits grows rapidly, and the marginal value of raw content diminishes + compared to pre-computed summaries that capture the essential information. +
+ +
+ The coordinator caches currentChatHashes and
+ currentContextItemIds. On each question, it checks whether the
+ context has changed or the hashes are empty. If neither condition is true,
+ initialization is skipped entirely and the question is streamed directly
+ using the existing hashes. This avoids redundant HTTP calls and backend
+ re-processing for follow-up questions within the same context.
+
boolean contextChanged = !newContextIds.equals(currentContextItemIds); +boolean needsInitialization = contextChanged || currentChatHashes.isEmpty();+ +
+ If the backend returns a "not found" error (indicating its cache was
+ cleared, e.g., after a restart), the coordinator clears
+ currentChatHashes, forcing re-initialization on the next
+ attempt. This ensures the system is self-healing without user intervention.
+
+ Every user question follows a well-defined lifecycle from keystroke to + rendered response. The lifecycle spans multiple threads and involves + careful coordination between Swing's EDT and background processing threads. +
+ +| Thread | +Responsibility | +
|---|---|
| EDT (Event Dispatch Thread) | +All Swing UI operations: reading input, rendering messages,
+ updating panels, dispatching callbacks via
+ SwingUtilities.invokeLater() |
+
| Background thread (anonymous) | +Backend initialization, HTTP requests, SSE stream reading, + token callback invocation | +
| Swing Timer (30ms) | +ChatStreamAnimator's teletype animation: dequeues tokens and + triggers re-renders | +
| Async save threads | +ConversationPersistence writes to disk without blocking the UI | +
+ Response processing converts raw SSE events from the backend into rendered + markdown in the chat panel. This involves three stages: SSE parsing, + token animation, and markdown rendering. +
+ +
+ The AIBackendClient.processSseStream() method reads
+ data: lines from the HTTP input stream. Each JSON payload has
+ a type field that determines how the content is formatted:
+
| SSE Type | Behavior |
|---|---|
thinking | Wrapped in [[AI_THINKING]]...[[/AI_THINKING]] markers for collapsible rendering. |
thinking_done | Closes the thinking block. |
status | Formatted as bold inside italic markdown: _**[Status]:** content_ |
final | First token gets **[FINAL ANSWER]:** prefix; subsequent tokens pass through. |
error | Throws AIBackendException. |
[DONE] or empty | Ignored (keep-alive / termination signal). |
+ Tokens flow from the background thread to the EDT via
+ SwingUtilities.invokeLater(). On the EDT,
+ ChatAreaPanel.enqueueStreamingToken() forwards the token to
+ ChatStreamAnimator.enqueueToken(), which splits it into
+ whitespace-delimited parts using the regex \S+|\s+ and adds
+ them to an internal queue.
+
+ A Swing Timer fires every 30ms. Each tick pops one part from
+ the queue, appends it to the draft message via
+ AIChatMessage.appendContent(), and triggers
+ AIMarkdownRenderer.renderDraft().
+
+ AIMarkdownRenderer renders the full draft on every tick. It
+ first removes the previous draft range from the StyledDocument,
+ then re-renders the complete message. The renderer supports:
+
#) — bold, 14pt, blue**text**) and italic (*text*)> text) — gray foreground- , * , 1. ) — bullet prefix<<hash-chunkId>>) — blue underlined links[[AI_THINKING]]...[[/AI_THINKING]]) — collapsible+ The draft goes through three states: +
+renderDraft() re-renders it on every tick.commitDraft() finalizes the
+ rendered content, making it permanent in the document.discardDraft() removes the
+ draft range from the document (used when the response is empty or on error).+ Citations allow the LLM to reference specific forensic items in its + responses. When a user clicks a citation, IPED navigates to the referenced + item and opens it in the viewer. +
+ +
+ Citations use the format <<hash-chunkId>>, where:
+
hash is the content hash of the source file (used to
+ search IPED's Lucene index).chunkId is the HTML element ID within the chat viewer,
+ used for scroll-to-position.
+ When AIMarkdownRenderer encounters << in
+ the response text, it scans for the closing >>, splits
+ on the first -, and creates a styled token with metadata
+ attributes:
+
TOKEN_ATTRIBUTE = Boolean.TRUETOKEN_HASH_ATTRIBUTE = the hash stringTOKEN_CHUNK_ID_ATTRIBUTE = the chunk ID string
+ The visible text is resolved via resolveTokenVisibleText(),
+ which looks up the hash in AIContextManager.getContextEntriesForUI()
+ and displays the filename if a match is found. Otherwise, the raw
+ <<hash-chunkId>> string is shown.
+
+ When a user clicks a citation token, ChatAreaPanel's mouse
+ listener reads the token attributes from the styled document element at the
+ click offset and invokes the navigationCallback. This calls
+ AIAssistantController.navigateToItem(), which:
+
hash: + the hash value.IItemId.chunkId is non-empty, tells the HTML viewer to scroll
+ to that element.FileProcessor and executes it to open the file
+ in the viewer.+ A detailed Mermaid sequence diagram covering all 12 flows of the chatbot + feature is maintained as a separate document: +
+
+
+ Chatbot sequence diagrams.html
+
+
+ That document covers: App Boot, Add File to Context, Auto-Fork, Send + Question / Streaming, Backend Recovery, Auto-Generate Title, Quick Actions, + Switch Conversation, Click Citation, Start New Chat, Persistence, and + Delete Conversation. Each flow includes a self-contained Mermaid diagram + with numbered steps. +
++ The following simplified diagram shows the core send-and-stream path that + ties together all three backend routes: +
+ ++ A single endpoint that accepts both HTML and summaries would simplify the + client but would complicate the backend: it would need to inspect the + payload type, branch on content format, and manage two distinct processing + pipelines behind a single URL. Separating the endpoints makes the backend + stateless and deterministic. The client pays a small cost in routing + logic, but the backend remains simple and testable. +
+ ++ Pre-computed summaries lose nuance. For a single chat or a small collection, + the full HTML preserves every message, every timestamp, and every media + reference. This maximizes answer quality when the data fits within the + context window. The threshold of 10 chunks is the empirical boundary where + raw content typically fits without truncation. +
+ ++ Sending raw HTML for 20+ chats would exceed the LLM's token limit and + cause the backend to crash or silently truncate. Summaries compress the + essential information into a fraction of the tokens while preserving + the chat structure and key events. The trade-off is reduced detail, but + this is acceptable for multi-chat queries where the user is typically + asking broad questions across conversations. +
+ ++ Backend initialization (parsing HTML, building vector embeddings, indexing) + is expensive. Caching the hash avoids re-processing when the user asks + follow-up questions within the same context. The cache is invalidated + automatically when the context changes or when the backend reports a + "not found" error. +
+ ++ Modifying the context after the LLM has processed it would create an + inconsistent state: the backend's cached session would reflect the old + context, but the UI would show the new one. Auto-fork avoids this by + creating a clean break: the new conversation starts fresh with the merged + context, and the old conversation remains as a historical record. +
+ +
+ Forensic data must be preserved for auditability. Soft-deleting conversations
+ (rewriting the JSON with status = "deleted") ensures the file
+ remains on disk for chain-of-custody purposes while being hidden from the
+ UI.
+
+ The coordinator preserves initialization state during streaming failures. If + streaming fails after a successful init, the hashes are kept so the user + can retry without re-uploading. Only initialization failures clear the + cache. This design prioritizes resilience: partial progress is never lost + unless the initialization itself failed. +
+ +
+ The ContextItemValidator currently rejects non-WhatsApp items.
+ To support Telegram, SMS, or other messaging formats, you would:
+
AITelegramChatExtractor)
+ parallel to AIWhatsappChatExtractor.ContextItemValidator.getRejectionReason() to accept
+ the new media type.AIPayloadFactory to route to the correct extractor.+ The rest of the pipeline (validation, context management, backend + communication, streaming) is format-agnostic and would require no changes. +
+ +
+ The AIBackendService interface defines six methods. Adding a
+ new endpoint requires:
+
AIBackendClient.AIChatCoordinator.askQuestion().
+ The threshold of 10 chunks is hardcoded in
+ AIChatCoordinator.askQuestion() and in the UI's
+ setSummarizedMode() call. Making it configurable would
+ require extracting it to AIBackendConfig or a system property.
+
+ The <<hash-chunkId>> format is parsed by
+ AIMarkdownRenderer.appendInlineMarkdown(). Changing the format
+ would require updating the regex scan and the token-attribute storage, as
+ well as the click handler in ChatAreaPanel.
+
+ If the backend switches from SSE to WebSocket or another streaming protocol,
+ only AIBackendClient.processSseStream() and the corresponding
+ streamChatResponse() / streamMultiChatResponse()
+ methods need updating. The rest of the system consumes tokens through the
+ same Consumer<String> callback interface.
+
| File | +Package | +Role | +
|---|---|---|
AIAssistantController.java | iped.app.ui.ai.controller | Main controller, event routing, chat lifecycle |
AIChatCoordinator.java | iped.app.ui.ai | Pipeline orchestrator, endpoint selection, session caching |
AIBackendClient.java | iped.app.ui.ai.backend | HTTP/SSE client, SSE parsing |
AIBackendService.java | iped.app.ui.ai.backend | Interface defining 6 backend endpoints |
AIBackendConfig.java | iped.app.ui.ai.backend | Immutable baseUrl + apiKey config |
AIBackendException.java | iped.app.ui.ai.backend | Checked exception for backend errors |
AIInitChatRequest.java | iped.app.ui.ai.backend | Single-chat init DTO |
AIStreamChatRequest.java | iped.app.ui.ai.backend | Stream request DTO with AIMessage inner class |
AIInitMultiChatRequest.java | iped.app.ui.ai.backend | Summarized multi-chat init DTO |
AIInitMultiChatFullRequest.java | iped.app.ui.ai.backend | Full HTML multi-chat init DTO |
AIMultiChatStreamRequest.java | iped.app.ui.ai.backend | Multi-chat stream request DTO |
AIPayloadFactory.java | iped.app.ui.ai.util | Builds DTOs from ContextFileEntry lists |
AIWhatsappChatExtractor.java | iped.app.ui.ai.util | Reads IPED items into UTF-8 HTML |
SummaryValueExtractor.java | iped.app.ui.ai.util | Extracts summaries from item metadata |
ContextItemValidator.java | iped.app.ui.ai.util | Validates items for context inclusion |
ConversationPersistence.java | iped.app.ui.ai.util | JSON serialization to/from disk |
ContextFileEntry.java | iped.app.ui.ai.model | Wraps IItem with validation state and summary |
AIChatMessage.java | iped.app.ui.ai.model | UI chat message model |
StandardConversation.java | iped.app.ui.ai.model | Chatbot conversation with chatHashes and contextIds |
AgentConversation.java | iped.app.ui.ai.model | Agent conversation with sessionId |
AIContextManager.java | iped.app.ui.ai.context | Thread-safe context file list |
ConversationManager.java | iped.app.ui.ai.context | Active conversation, auto-title, conversation list |
ChatAreaPanel.java | iped.app.ui.ai.view | Chat rendering, streaming orchestration, citation clicks |
ChatStreamAnimator.java | iped.app.ui.ai.view | Timer-based teletype animation |
AIMarkdownRenderer.java | iped.app.ui.ai.view | Markdown-to-StyledDocument, citations, thinking blocks |
+ The IPED AI Assistant Chatbot is designed around a central trade-off: + answer quality versus data volume. For small contexts, raw HTML provides the + LLM with complete information. For large contexts, pre-computed summaries + prevent token overflow while preserving essential information. The three-path + routing strategy, driven by chunk count, makes this adaptation automatic + and transparent to the user. +
++ The architecture separates concerns cleanly: the view layer handles + rendering, the controller manages lifecycle, the coordinator orchestrates + the pipeline, the payload factory translates data formats, and the network + client handles HTTP communication. This separation makes each component + independently testable and replaceable. +
++ Error recovery is designed to be self-healing: cached hashes are preserved + during streaming failures and invalidated automatically on backend cache + misses. Partial drafts are salvaged on error so the user never loses the + LLM's partial response. +
++ The citation system bridges the gap between the LLM's text output and + IPED's forensic navigation, enabling investigators to move seamlessly from + a natural-language answer to the underlying evidence. +
++ This document reflects the verified architecture of the AI Assistant module, + reconciled against the actual source code as of July 2026. +
+ ++ AIAssistantPanel: top-level JFrame container (singleton), + layout manager, glass-pane processing lock. +
++ HeaderPanel: title, backend status, sidebar toggle. +
+SidebarPanel: chat history list and selection UI.
++ ContextPanel: file context list, per-item removal, + context-edit locking, summarized-mode indicator. +
++ ChatAreaPanel: chat rendering, input, streaming + orchestration, citation click handling. +
++ AIMarkdownRenderer: markdown-to-StyledDocument parser, + thinking blocks, citation tokens. +
++ ChatStreamAnimator: timer-based teletype animation + engine, token queue management. +
++ AIAssistantController: orchestrates event routing, + view updates, chat lifecycle, and persistence. +
++ AIChatCoordinator: central pipeline — validates + context entries, routes to the correct backend endpoint based on + chunk count, manages session hashes, streams responses. +
+
+ AIPayloadFactory: translates
+ ContextFileEntry lists into backend DTOs
+ (AIInitMultiChatRequest,
+ AIInitMultiChatFullRequest).
+
+ AIWhatsappChatExtractor: reads item
+ InputStream into UTF-8 HTML.
+
+ SummaryValueExtractor: extracts AI summaries from item + extra attributes or Lucene metadata. +
++ ContextItemValidator: rejects non-WhatsApp items, empty + files, empty communications. +
+
+ AIBackendClient: HTTP/SSE client implementing
+ AIBackendService. Six endpoints: /api/init_chat,
+ /api/chat/stream,
+ /api/init_multichat_with_summaries,
+ /api/multichat/stream,
+ /api/init_multichat_full,
+ /api/multichat_full/stream.
+
+ AIBackendConfig: immutable baseUrl +
+ apiKey.
+
+ DTOs: AIInitChatRequest,
+ AIStreamChatRequest,
+ AIInitMultiChatRequest,
+ AIInitMultiChatFullRequest,
+ AIMultiChatStreamRequest.
+
+ AIContextManager: singleton, thread-safe context file + list with change-event broadcasting. +
++ ConversationManager: singleton, active conversation + state, auto-title generation, conversation list. +
+
+ ConversationPersistence: JSON serialization to/from
+ <case_dir>/iped/data/ai_chats/. Load, save,
+ soft-delete.
+
+ This flow shows how the AI Assistant restores persisted conversations, + initializes controller dependencies, and rebuilds the UI from the current + manager state. +
+
+ Context additions append files to the current active chat while the
+ conversation is still directly editable (no assistant reply yet). Files are
+ validated by ContextItemValidator and appended to the
+ AIContextManager.
+
+ When the active conversation already has an assistant reply, its context + is no longer directly editable. Adding files at this point starts a new + conversation that carries forward the existing context and merges the newly + selected files. +
+
+ Message sending delegates UI events to
+ AIAssistantController, which routes through
+ AIChatCoordinator. The coordinator validates context entries,
+ computes the total chunk count, and selects one of three backend paths:
+
initChat + streamChatResponseAIPayloadFactory.buildMultiChatFullRequest, calls initMultiChatFull + streamMultiChatFullResponseAIPayloadFactory.buildMultiChatRequest, calls initMultiChat + streamMultiChatResponse
+ Tokens stream through AIBackendClient (SSE) →
+ ChatStreamAnimator (timer queue) →
+ AIMarkdownRenderer (StyledDocument). The draft is committed
+ only after the stream drains.
+
+ The coordinator preserves successful initialization state during + streaming failures. If streaming fails mid-response, the UI salvages + whatever partial text the LLM managed to generate, commits it to memory, + and appends a system error message so the user doesn't lose their data. +
+
+ Title generation is centralized inside ConversationManager.
+ When the first user message is added to a conversation whose title is still
+ the default "New Conversation", a substring of the message is
+ used as the title.
+
+ Quick Action buttons inject a prebuilt prompt into the chat input and + reuse the existing send flow. +
++ Conversation switching restores visible chat state immediately, clears any + active draft and current context, loads historical coordinator state, then + performs a background recovery of persisted context items before replaying + coordinator state with refreshed IDs. +
+
+ A citation click is handled in ChatAreaPanel and delegated
+ to the controller for forensic item navigation.
+
+ New chat creation is handled by the controller before clearing UI and + context state. After the reset, the controller appends a system + message announcing that a new conversation session has started. +
++ Conversations are saved to disk on every meaningful change. Persistence + is asynchronous (background threads). Deleted chats are soft-deleted. +
++ Deletion marks the conversation as deleted (soft-delete) and keeps the + JSON file for auditability. If the deleted conversation is the active one, + the controller loads the next remaining conversation or clears the screen + if none remain. +
++ * Provides convenient methods for displaying file names, paths, and labels + * in the UI, as well as proper {@code equals} and {@code hashCode} implementations + * based on the unique item ID. + *
+ */ +public class ContextFileEntry { + + /** The underlying file item */ + private final IItem item; + + /** The AI-generated summary of the item (if available) */ + private final String summary; + + /** Whether this entry is valid to be sent to AI context payload */ + private final boolean validForContext; + + /** Validation reason shown in UI when entry is invalid */ + private final String validationReason; + + /** + * Constructs a new {@code ContextFileEntry} wrapping the given {@link IItem}. + * + * @param item the item to wrap + */ + public ContextFileEntry(IItem item) { + this(item, SummaryValueExtractor.extractSummary(item), true, null); + } + + /** + * Constructs a new {@code ContextFileEntry} wrapping the given {@link IItem} with explicit summary. + * + * @param item the item to wrap + * @param summary the AI-generated summary (can be null) + */ + public ContextFileEntry(IItem item, String summary) { + this(item, summary, true, null); + } + + private ContextFileEntry(IItem item, String summary, boolean validForContext, String validationReason) { + this.item = item; + this.summary = summary; + this.validForContext = validForContext; + this.validationReason = validationReason; + } + + /** + * Creates an invalid entry used only for UI feedback. + * + * @param item the item that failed validation + * @param reason the reason shown to the user + * @return invalid context entry + */ + public static ContextFileEntry invalid(IItem item, String reason) { + return new ContextFileEntry(item, SummaryValueExtractor.extractSummary(item), false, reason); + } + + /** + * Returns the underlying {@link IItem}. + * + * @return the wrapped item + */ + public IItem getItem() { + return item; + } + + /** + * Returns the AI-generated summary if available. + * + * @return the summary, or null if not available + */ + public String getSummary() { + return summary; + } + + /** + * Returns true if this entry has an AI-generated summary. + * + * @return true if summary exists + */ + public boolean hasSummary() { + return summary != null && !summary.trim().isEmpty(); + } + + /** + * Returns true when the entry is valid for AI context payload. + */ + public boolean isValidForContext() { + return validForContext; + } + + /** + * Returns UI validation reason for invalid entries. + */ + public String getValidationReason() { + return validationReason; + } + + /** + * Returns the displayable file name, or "Unknown File" if {@code null}. + * + * @return file name + */ + public String getFileName() { + return item.getName() != null ? item.getName() : "Unknown File"; + } + + /** + * Returns the full file path, or an empty string if {@code null}. + * + * @return full path + */ + public String getFullPath() { + return item.getPath() != null ? item.getPath() : ""; + } + + @Override + public String toString() { + if (!validForContext && validationReason != null && !validationReason.trim().isEmpty()) { + return getFileName() + " - " + validationReason; + } + if (hasSummary()) { + return getFileName() + " [Summary]"; + } + return getFileName(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ContextFileEntry)) return false; + return this.item.getId() == ((ContextFileEntry) o).item.getId(); + } + + @Override + public int hashCode() { + // Use Integer wrapper to get the hashcode of a primitive int + return Integer.hashCode(item.getId()); + } +} \ No newline at end of file diff --git a/iped-app/src/main/java/iped/app/ui/ai/model/Conversation.java b/iped-app/src/main/java/iped/app/ui/ai/model/Conversation.java new file mode 100644 index 0000000000..b1b8e2b43f --- /dev/null +++ b/iped-app/src/main/java/iped/app/ui/ai/model/Conversation.java @@ -0,0 +1,55 @@ +package iped.app.ui.ai.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public abstract class Conversation { + private String id; + private String title; + private String status; // e.g. "active", "deleted" + private long createdAt; + private long lastModified; + private List+ * This class acts as a boundary layer. It ensures that the network client + * never needs to know about IPED-specific classes (like {@link ContextFileEntry}), + * and enforces strict validation rules before data is allowed to leave the application + *
+ */ +public class AIPayloadFactory { + + /** + * Builds the Multi-Chat initialization payload by extracting summaries from the UI context. + *+ * Memory Guardrail: This method aggressively filters out files that do not + * have pre-computed summaries. Sending raw HTML for multiple files would instantly + * exceed the LLM's token context window and crash the backend. + *
+ * * @param entries The raw list of files currently held in the UI's {@link AIContextManager}. + * @return A clean, populated DTO ready for Gson serialization. + * @throws IllegalArgumentException if the provided list yields zero valid summaries, + * meaning a Multi-Chat session cannot be legally formed. + */ + public static AIInitMultiChatRequest buildMultiChatRequest(List+ * This class acts as a boundary layer between IPED's raw file storage and the + * AI Backend payload builders. It ensures that only potentially valid text data + * is loaded into memory and sent across the network. + *
+ */ +public class AIWhatsappChatExtractor { + + private final String whatsAppChatContentType = WhatsAppParser.WHATSAPP_CHAT.toString(); + + /** + *+ * Basic validation to check whether the item is a WhatsApp chat by content type. + * Handles standard files and virtual files extracted from databases (which may lack extensions). + *
+ * @param item The IPED evidence item to inspect.
+ * @return true if the file indicates WhatsApp chat content; false otherwise.
+ */
+ public boolean isWhatsAppChatType(IItem item) {
+ if (item == null) {
+ return false;
+ }
+
+ if (item.getMediaType() != null && whatsAppChatContentType.equals(item.getMediaType().toString())) {
+ return true;
+ }
+
+ return item.getMetadata() != null
+ && whatsAppChatContentType.equals(item.getMetadata().get(StandardParser.INDEXER_CONTENT_TYPE));
+ }
+
+ /**
+ * Extracts the raw file content from the IItem into a UTF-8 encoded String.
+ * @param item The IPED evidence item containing the chat log.
+ * @return The complete, raw HTML string.
+ * @throws IllegalArgumentException if the item is null.
+ * @throws IllegalStateException if the underlying file stream cannot be opened.
+ * @throws Exception if reading the stream fails due to an I/O error.
+ */
+ public String extractHtml(IItem item) throws Exception {
+ if (!isWhatsAppChatType(item)) {
+ throw new IllegalArgumentException("Selected file does not appear to be a WhatsApp chat export.");
+ }
+
+ // Try-with-resources ensures streams are closed automatically, preventing memory/file handle leaks.
+ try (InputStream is = item.getBufferedInputStream();
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
+
+ if (is == null) {
+ throw new IllegalStateException("Could not open input stream for item ID: " + item.getId());
+ }
+
+ int nRead;
+ byte[] data = new byte[16384]; // 16KB chunks
+
+ while ((nRead = is.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+
+ return new String(buffer.toByteArray(), StandardCharsets.UTF_8);
+ }
+ }
+}
\ No newline at end of file
diff --git a/iped-app/src/main/java/iped/app/ui/ai/util/ContextItemValidator.java b/iped-app/src/main/java/iped/app/ui/ai/util/ContextItemValidator.java
new file mode 100644
index 0000000000..1ce5c0ac64
--- /dev/null
+++ b/iped-app/src/main/java/iped/app/ui/ai/util/ContextItemValidator.java
@@ -0,0 +1,123 @@
+package iped.app.ui.ai.util;
+
+import iped.data.IItem;
+import iped.engine.lucene.analysis.CategoryTokenizer;
+import iped.properties.ExtraProperties;
+
+public class ContextItemValidator {
+
+ private final AIWhatsappChatExtractor chatExtractor;
+
+ public ContextItemValidator() {
+ this(new AIWhatsappChatExtractor());
+ }
+
+ public ContextItemValidator(AIWhatsappChatExtractor chatExtractor) {
+ this.chatExtractor = chatExtractor;
+ }
+
+ public String getRejectionReason(IItem item) {
+ if (!chatExtractor.isWhatsAppChatType(item)) {
+ return "Rejected: Not a WhatsApp chat item.";
+ }
+
+ if (hasEmptyFilesCategory(item)) {
+ return "Rejected: Category is Empty Files.";
+ }
+
+ if (SummaryValueExtractor.hasSummary(item)) {
+ return null;
+ }
+
+ Boolean isEmpty = readCommunicationIsEmpty(item);
+ if (Boolean.TRUE.equals(isEmpty)) {
+ return "Rejected: Communication is empty.";
+ }
+ return null;
+ }
+
+ private boolean hasEmptyFilesCategory(IItem item) {
+ if (item == null) {
+ return false;
+ }
+
+ if (item.getCategorySet() != null) {
+ for (String category : item.getCategorySet()) {
+ if (isEmptyFilesCategoryValue(category)) {
+ return true;
+ }
+ }
+ }
+
+ String categories = item.getCategories();
+ if (categories != null && !categories.isBlank()) {
+ String[] splitCategories = categories.split(String.valueOf(CategoryTokenizer.SEPARATOR));
+ for (String category : splitCategories) {
+ if (isEmptyFilesCategoryValue(category)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private boolean isEmptyFilesCategoryValue(String value) {
+ if (value == null) {
+ return false;
+ }
+
+ String normalized = value.trim().toLowerCase();
+ return normalized.equals("empty files") || normalized.contains("empty files");
+ }
+
+ private Boolean readCommunicationIsEmpty(IItem item) {
+ if (item == null) {
+ return null;
+ }
+
+ String[] keys = {
+ ExtraProperties.COMMUNICATION_PREFIX + "isEmpty"
+ };
+
+ for (String key : keys) {
+ String value = readFirstValue(item, key);
+ if (value != null) {
+ return Boolean.parseBoolean(value.trim().toLowerCase());
+ }
+ }
+ return null;
+ }
+
+ private String readFirstValue(IItem item, String key) {
+ Object extra = item.getExtraAttribute(key);
+ if (extra != null) {
+ if (extra instanceof String) {
+ return (String) extra;
+ }
+ if (extra instanceof Boolean) {
+ return String.valueOf(extra);
+ }
+ if (extra instanceof String[] && ((String[]) extra).length > 0) {
+ return ((String[]) extra)[0];
+ }
+ return String.valueOf(extra);
+ }
+
+ if (item.getMetadata() == null) {
+ return null;
+ }
+
+ String value = item.getMetadata().get(key);
+ if (value != null) {
+ return value;
+ }
+
+ String[] values = item.getMetadata().getValues(key);
+ if (values != null && values.length > 0) {
+ return values[0];
+ }
+
+ return null;
+ }
+}
diff --git a/iped-app/src/main/java/iped/app/ui/ai/util/ConversationPersistence.java b/iped-app/src/main/java/iped/app/ui/ai/util/ConversationPersistence.java
new file mode 100644
index 0000000000..29f2d007b9
--- /dev/null
+++ b/iped-app/src/main/java/iped/app/ui/ai/util/ConversationPersistence.java
@@ -0,0 +1,133 @@
+package iped.app.ui.ai.util;
+
+import iped.engine.data.IPEDMultiSource;
+import iped.app.ui.App;
+import iped.app.ui.ai.model.Conversation;
+import iped.app.ui.ai.model.StandardConversation;
+import iped.app.ui.ai.model.AgentConversation;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Handles saving and loading AI Chat conversations to/from the local IPED case directory.
+ * Ensures strict forensic isolation so chats do not bleed between different cases.
+ */
+public class ConversationPersistence {
+
+ // Subfolder inside the case's iped/data directory
+ private static final String CHATS_DIR_NAME = "iped/data/ai_chats";
+
+ // Pretty printing makes the JSON human-readable
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
+
+ /**
+ * Resolves the case-isolated directory for storing AI chats
+ * Creates the directory if it does not exist
+ * Safely handles IPEDMultiSource (Multi-case mode) by falling back to the first available case
+ */
+ private static File getStorageDirectory() {
+ try {
+
+ if (App.get() == null || App.get().appCase == null) {
+ return null; // Failsafe if accessed outside of an open case
+ }
+
+ File caseDir = App.get().appCase.getCaseDir();
+
+ // Handle the Multi-Case edge case where the global case directory might be null
+ if (caseDir == null && App.get().appCase instanceof IPEDMultiSource) {
+ IPEDMultiSource multiSource = (IPEDMultiSource) App.get().appCase;
+ if (!multiSource.getAtomicSources().isEmpty()) {
+ // Fallback: Save the multi-case chats into the first case's directory
+ caseDir = multiSource.getAtomicSources().get(0).getCaseDir();
+ }
+ }
+
+ // Absolute failsafe if the directory is still somehow unresolvable
+ if (caseDir == null) {
+ caseDir = new File(System.getProperty("user.home"), ".iped_ai_chats");
+ }
+
+ File chatsDir = new File(caseDir, CHATS_DIR_NAME);
+
+ if (!chatsDir.exists()) {
+ chatsDir.mkdirs();
+ }
+
+ return chatsDir;
+ } catch (Exception e) {
+ System.err.println("Safe fallback: Could not resolve AI storage dir - " + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Serializes a Conversation object to a distinct JSON file
+ */
+ public static void saveConversation(Conversation conversation) {
+ File dir = getStorageDirectory();
+ if (dir == null || conversation == null) return;
+
+ String prefix = conversation.isAgentConversation() ? "agent_" : "chat_";
+ File chatFile = new File(dir, prefix + conversation.getId() + ".json");
+
+ try (FileWriter writer = new FileWriter(chatFile)) {
+ GSON.toJson(conversation, writer);
+ } catch (IOException e) {
+ System.err.println("Failed to save AI conversation: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Reads all JSON files in the case's chat directory and returns them sorted by newest first.
+ */
+ public static List
+ * This class acts strictly as the top-level Container (View) in the MVP/MVC architecture.
+ * Responsibility is isolated entirely to layout arrangement and window lifecycle management,
+ * completely decoupled from business, extraction, and network logic.
+ *