Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/src/main/java/ai/javaclaw/JavaClawApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.DefaultApplicationArguments;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
Expand Down Expand Up @@ -44,12 +45,16 @@ public void run(ApplicationArguments args) throws Exception {

@EventListener
public void on(ConfigurationChangedEvent configurationChangedEvent) {
ApplicationArguments args = applicationContext.getBean(ApplicationArguments.class);


Thread thread = new Thread(() -> {
try {
ApplicationArguments args = new DefaultApplicationArguments();
if(applicationContext != null){
args = applicationContext.getBean(ApplicationArguments.class);
applicationContext.close();
}
Thread.sleep(2000);
applicationContext.close();
applicationContext = SpringApplication.run(JavaClawApplication.class, args.getSourceArgs());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Expand Down
54 changes: 54 additions & 0 deletions app/src/main/java/ai/javaclaw/api/AgentsController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ai.javaclaw.api;

import ai.javaclaw.agents.AgentRegistry;
import ai.javaclaw.agents.AgentWorkspaceResolver;
import ai.javaclaw.agents.ConfiguredAgent;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.nio.file.Path;
import java.util.List;

@Controller
public class AgentsController {

private final AgentRegistry agentRegistry;
private final AgentWorkspaceResolver agentWorkspaceResolver;

public AgentsController(AgentRegistry agentRegistry, AgentWorkspaceResolver agentWorkspaceResolver) {
this.agentRegistry = agentRegistry;
this.agentWorkspaceResolver = agentWorkspaceResolver;
}

@GetMapping("/agents")
public String agents(Model model) {
List<AgentView> agents = agentRegistry.getAgents().stream()
.map(configuredAgent -> new AgentView(
configuredAgent.id(),
configuredAgent.provider(),
configuredAgent.model(),
resolveWorkspace(configuredAgent),
configuredAgent.id().equals(agentRegistry.getDefaultAgentId())
))
.toList();

model.addAttribute("agents", agents);
model.addAttribute("hasAgents", !agents.isEmpty());
return "agents";
}

private String resolveWorkspace(ConfiguredAgent configuredAgent) {
Path workspacePath = agentWorkspaceResolver.resolveWorkspacePath(configuredAgent.workspacePath(), configuredAgent.id());
return workspacePath.toString();
}

public record AgentView(
String id,
String provider,
String model,
String workspace,
boolean isDefault
) {
}
}
30 changes: 24 additions & 6 deletions app/src/main/java/ai/javaclaw/chat/ChatChannel.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ai.javaclaw.chat;

import ai.javaclaw.agent.Agent;
import ai.javaclaw.agents.AgentConversationId;
import ai.javaclaw.agents.AgentRegistry;
import ai.javaclaw.channels.Channel;
import ai.javaclaw.channels.ChannelMessageReceivedEvent;
import ai.javaclaw.channels.ChannelRegistry;
Expand Down Expand Up @@ -36,13 +38,15 @@ public class ChatChannel implements Channel {
private static final Logger log = LoggerFactory.getLogger(ChatChannel.class);

private final Agent agent;
private final AgentRegistry agentRegistry;
private final ChannelRegistry channelRegistry;
private final ChatMemoryRepository chatMemoryRepository;
private final ConcurrentLinkedQueue<String> pendingMessages = new ConcurrentLinkedQueue<>();
private final AtomicReference<WebSocketSession> wsSession = new AtomicReference<>();

public ChatChannel(Agent agent, ChannelRegistry channelRegistry, ChatMemoryRepository chatMemoryRepository) {
public ChatChannel(Agent agent, AgentRegistry agentRegistry, ChannelRegistry channelRegistry, ChatMemoryRepository chatMemoryRepository) {
this.agent = agent;
this.agentRegistry = agentRegistry;
this.channelRegistry = channelRegistry;
this.chatMemoryRepository = chatMemoryRepository;
channelRegistry.registerChannel(this);
Expand Down Expand Up @@ -96,10 +100,24 @@ public void sendMessage(String message) {
/**
* Returns all known conversation IDs, always with "web" first.
*/
public List<String> conversationIds() {
public List<String> agentIds() {
List<String> ids = agentRegistry.getAgents().stream().map(ai.javaclaw.agents.ConfiguredAgent::id).toList();
if (!ids.isEmpty()) {
return ids;
}
return List.of(agentRegistry.getDefaultAgentId());
}

public String defaultAgentId() {
return agentRegistry.getDefaultAgentId();
}

public List<String> conversationIds(String agentId) {
List<String> result = new ArrayList<>();
result.add("web");
chatMemoryRepository.findConversationIds().stream()
.filter(id -> agentId.equals(AgentConversationId.agentId(id)))
.map(AgentConversationId::rawConversationId)
.filter(id -> !id.equals("web"))
.forEach(result::add);
return result;
Expand All @@ -109,8 +127,8 @@ public List<String> conversationIds() {
* Loads conversation history for the given conversationId as HTML bubbles.
* Returns a single welcome bubble if no history exists yet.
*/
public List<String> loadHistoryAsHtml(String conversationId) {
List<Message> history = chatMemoryRepository.findByConversationId(conversationId);
public List<String> loadHistoryAsHtml(String agentId, String conversationId) {
List<Message> history = chatMemoryRepository.findByConversationId(AgentConversationId.scoped(agentId, conversationId));
if (history.isEmpty()) {
return List.of(ChatHtml.agentBubble("Hi! I'm your JavaClaw assistant. How can I help you today?"));
}
Expand All @@ -125,9 +143,9 @@ public List<String> loadHistoryAsHtml(String conversationId) {
/**
* Handles a chat message from the web UI for the given conversationId.
*/
public String chat(String conversationId, String message) {
public String chat(String agentId, String conversationId, String message) {
channelRegistry.publishMessageReceivedEvent(new ChannelMessageReceivedEvent(getName(), message));
return agent.respondTo(conversationId, message);
return agent.respondTo(agentId, conversationId, message);
}

private static String buildBackgroundMessageHtml(String text) {
Expand Down
23 changes: 19 additions & 4 deletions app/src/main/java/ai/javaclaw/chat/ChatHtml.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ public static String typingDots() {
</div>""";
}

public static String chatInputArea(String conversationId) {
public static String chatInputArea(String agentId, String conversationId) {
if ("web".equals(conversationId)) {
return """
<form id="chat-form" ws-send hx-boost="false"
hx-vals='js:{"type": "userMessage", "conversationId": document.getElementById("channel-select") ? document.getElementById("channel-select").value : "web"}'>
hx-vals='js:{"type": "userMessage", "agentId": document.getElementById("agent-select") ? document.getElementById("agent-select").value : "%s", "conversationId": document.getElementById("channel-select") ? document.getElementById("channel-select").value : "web"}'>
<div class="field is-grouped" style="align-items: flex-end; margin: 0;">
<div class="control is-expanded">
<textarea id="message-input" class="textarea" name="message" rows="1"
Expand All @@ -57,7 +57,7 @@ public static String chatInputArea(String conversationId) {
</button>
</div>
</div>
</form>""";
</form>""".formatted(HtmlUtils.htmlEscape(agentId));
}
String label = HtmlUtils.htmlEscape(labelFor(conversationId));
return """
Expand All @@ -70,7 +70,7 @@ public static String conversationSelector(List<String> ids, String selectedId) {
sb.append("""
<select id="channel-select" class="select" name="conversationId" \
ws-send hx-trigger="change" \
hx-vals='{"type": "channelChanged"}'>""");
hx-vals='js:{"type": "channelChanged", "agentId": document.getElementById("agent-select") ? document.getElementById("agent-select").value : ""}'>""");
for (String id : ids) {
sb.append("<option value=\"").append(HtmlUtils.htmlEscape(id)).append("\"");
if (id.equals(selectedId)) sb.append(" selected");
Expand All @@ -80,6 +80,21 @@ public static String conversationSelector(List<String> ids, String selectedId) {
return sb.toString();
}

public static String agentSelector(List<String> ids, String selectedId) {
StringBuilder sb = new StringBuilder();
sb.append("""
<select id="agent-select" class="select" name="agentId" \
ws-send hx-trigger="change" \
hx-vals='{"type": "agentChanged"}'>""");
for (String id : ids) {
sb.append("<option value=\"").append(HtmlUtils.htmlEscape(id)).append("\"");
if (id.equals(selectedId)) sb.append(" selected");
sb.append(">").append(HtmlUtils.htmlEscape(id)).append("</option>");
}
sb.append("</select>");
return sb.toString();
}

private static String labelFor(String conversationId) {
if ("web".equals(conversationId)) return "Web Chat";
if (conversationId.startsWith("discord-")) return "Discord (" + conversationId.substring("discord-".length()) + ")";
Expand Down
51 changes: 40 additions & 11 deletions app/src/main/java/ai/javaclaw/chat/ws/ChatWebSocketHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ public void afterConnectionEstablished(WebSocketSession session) throws Exceptio
chatChannel.setWsSession(session);
log.info("WebChat WebSocket connected: {}", session.getId());

List<String> ids = chatChannel.conversationIds();
String selectedId = ids.getFirst();

String conversationSelector = ChatHtml.conversationSelector(ids, selectedId);
String bubbles = String.join(System.lineSeparator(), chatChannel.loadHistoryAsHtml(selectedId));
String inputArea = ChatHtml.chatInputArea(selectedId);
List<String> agentIds = chatChannel.agentIds();
String selectedAgentId = agentIds.contains(chatChannel.defaultAgentId()) ? chatChannel.defaultAgentId() : agentIds.getFirst();
List<String> conversationIds = chatChannel.conversationIds(selectedAgentId);
String selectedConversationId = conversationIds.getFirst();

String agentSelector = ChatHtml.agentSelector(agentIds, selectedAgentId);
String conversationSelector = ChatHtml.conversationSelector(conversationIds, selectedConversationId);
String bubbles = String.join(System.lineSeparator(), chatChannel.loadHistoryAsHtml(selectedAgentId, selectedConversationId));
String inputArea = ChatHtml.chatInputArea(selectedAgentId, selectedConversationId);
chatChannel.sendHtml(
Htmx.oobInnerHtml("agent-selector", agentSelector),
Htmx.oobInnerHtml("channel-selector", conversationSelector),
Htmx.oobInnerHtml("chat-messages", bubbles),
Htmx.oobInnerHtml("chat-input-area", inputArea));
Expand All @@ -61,29 +65,54 @@ protected void handleTextMessage(WebSocketSession session, TextMessage message)

if ("channelChanged".equals(type)) {
handleChannelChanged(payload);
} else if ("agentChanged".equals(type)) {
handleAgentChanged(payload);
} else if ("userMessage".equals(type)) {
handleUserMessage(payload);
}
}

private void handleAgentChanged(Map<String, Object> payload) throws Exception {
String agentId = (String) payload.get("agentId");
if (agentId == null || agentId.isBlank()) {
agentId = chatChannel.defaultAgentId();
}

List<String> conversationIds = chatChannel.conversationIds(agentId);
String selectedConversationId = conversationIds.getFirst();
String conversationSelector = ChatHtml.conversationSelector(conversationIds, selectedConversationId);
String bubbles = String.join(System.lineSeparator(), chatChannel.loadHistoryAsHtml(agentId, selectedConversationId));
String inputArea = ChatHtml.chatInputArea(agentId, selectedConversationId);
chatChannel.sendHtml(
Htmx.oobInnerHtml("channel-selector", conversationSelector),
Htmx.oobInnerHtml("chat-messages", bubbles),
Htmx.oobInnerHtml("chat-input-area", inputArea));
}

private void handleChannelChanged(Map<String, Object> payload) throws Exception {
String agentId = (String) payload.get("agentId");
String conversationId = (String) payload.get("conversationId");
if (conversationId == null || conversationId.isBlank()) return;
if (agentId == null || agentId.isBlank()) {
agentId = chatChannel.defaultAgentId();
}

String bubbles = String.join(System.lineSeparator(), chatChannel.loadHistoryAsHtml(conversationId));
String inputArea = ChatHtml.chatInputArea(conversationId);
String bubbles = String.join(System.lineSeparator(), chatChannel.loadHistoryAsHtml(agentId, conversationId));
String inputArea = ChatHtml.chatInputArea(agentId, conversationId);
chatChannel.sendHtml(
Htmx.oobInnerHtml("chat-messages", bubbles),
Htmx.oobInnerHtml("chat-input-area", inputArea));
}

private void handleUserMessage(Map<String, Object> payload) throws Exception {
String agentId = (String) payload.get("agentId");
String conversationId = (String) payload.get("conversationId");
String userMessage = (String) payload.get("message");

if (userMessage == null || userMessage.isBlank()) return;
userMessage = userMessage.trim();
if (conversationId == null || conversationId.isBlank()) conversationId = "web";
if (agentId == null || agentId.isBlank()) agentId = chatChannel.defaultAgentId();

// Echo user message + show typing indicator
chatChannel.sendHtml(
Expand All @@ -92,12 +121,12 @@ private void handleUserMessage(Map<String, Object> payload) throws Exception {

try {
// Call agent (blocking — background tasks may push messages via ChatChannel during this)
String response = chatChannel.chat(conversationId, userMessage);
String response = chatChannel.chat(agentId, conversationId, userMessage);
chatChannel.sendHtml(
Htmx.oobAppend("chat-messages", ChatHtml.agentBubble(response)),
Htmx.oobReplace("typing-indicator", ""));
} catch (RuntimeException ex) {
log.warn("Chat request failed for conversation {}", conversationId, ex);
log.warn("Chat request failed for agent {} conversation {}", agentId, conversationId, ex);
chatChannel.sendHtml(
Htmx.oobAppend("chat-messages", ChatHtml.agentBubble(genericUserFacingError(ex))),
Htmx.oobReplace("typing-indicator", ""));
Expand All @@ -116,4 +145,4 @@ private static String summarizeError(Throwable ex) {

return message;
}
}
}
Loading
Loading