Skip to content

Commit 6beb52a

Browse files
committed
system instructions refinement, agent can now span another aggent
1 parent 10209fc commit 6beb52a

13 files changed

Lines changed: 363 additions & 302 deletions

src/main/java/uno/anahata/ai/Chat.java

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import uno.anahata.ai.tools.MultiPartResponse;
2929
import uno.anahata.ai.tools.ToolCallOutcome;
3030
import uno.anahata.ai.tools.ToolCallStatus;
31+
import uno.anahata.ai.tools.UserFeedback;
3132
import uno.anahata.ai.internal.GsonUtils;
3233
import uno.anahata.ai.internal.PartUtils;
3334
import uno.anahata.ai.status.ChatStatus;
@@ -359,28 +360,10 @@ private boolean processAndReloopForFunctionCalls(ChatMessage modelMessageWithCal
359360

360361
// Always create a feedback message if there were any tool calls proposed.
361362
if (!processingResult.getOutcomes().isEmpty()) {
362-
String toolFeedback = processingResult.getOutcomes().stream()
363-
.map(outcome -> {
364-
String toolName = outcome.getIdentifiedCall().getCall().name().orElse("unknown");
365-
String id = outcome.getIdentifiedCall().getId();
366-
String status = outcome.getStatus().name();
367-
/*
368-
if (outcome.getStatus() == ToolCallStatus.YES || outcome.getStatus() == ToolCallStatus.ALWAYS) {
369-
status = "";
370-
} */
371-
372-
return String.format("[%s id=%s %s]", toolName, id, status);
373-
})
374-
.collect(Collectors.joining(" "));
375-
376-
StringBuilder feedbackText = new StringBuilder("Tool Feedback: ").append(toolFeedback);
377-
378-
if (StringUtils.isNotBlank(processingResult.getUserComment())) {
379-
feedbackText.append("\nUser Comment: '").append(processingResult.getUserComment()).append("'");
380-
}
363+
String feedbackText = processingResult.getFeedbackMessage();
381364

382365
List<Part> userFeedbackParts = new ArrayList<>();
383-
userFeedbackParts.add(Part.fromText(feedbackText.toString()));
366+
userFeedbackParts.add(Part.fromText(feedbackText));
384367
userFeedbackParts.addAll(extraUserParts);
385368

386369
Content feedbackContent = Content.builder().role("user").parts(userFeedbackParts).build();

src/main/java/uno/anahata/ai/context/provider/spi/ContextSummaryProvider.java

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@
3232
* of the entire conversation history into the model's prompt.
3333
* <p>
3434
* This summary includes unique IDs for every message and part, allowing the
35-
* model to perform precise context pruning and management. It also provides
36-
* instructions on the "Prune-As-You-Go" protocol.
35+
* model to perform precise context pruning and management.
3736
* </p>
3837
*/
3938
public class ContextSummaryProvider extends ContextProvider {
@@ -106,36 +105,9 @@ public List<Part> getParts(Chat chat) {
106105
chat.getContextManager().getTokenThreshold()
107106
));
108107
sb.append("\n");
109-
sb.append("The following table is the Output of ContextWindow.getContextSummary() with the unique id of every part, every stateful resource and every tool call pair (request response) in this conversation so you can compress the context *as-you-go* or when explicitely instructed by the user. Dont expect the user to instruct you explicitely and dont expect the user to perform manual prunning either."
110-
+ "You have to work with the total token count (as given by the api on the last turn) and the threshold (max tokens) to work out how far you can go in adding to the context window."
111-
+ "Your ultimate optimal goal is to ensure that a conversation can "
112-
+ "\na)Run indefinetly (without ever hitting max tokens) with the most relevant information in the most relevant postion of the context"
113-
+ "\nb)Be as efficient as possible in terms of token usage and turns"
114-
+ "\nc)Be a smooth flowing experience to the point that the user wont even think about manual prunning or ask you to 'compress' the context."
115-
116-
+ "\n"
117-
+ "\n\nTool execution results are sent inmeditaly (sometimes without the user having a chance to add a message) so: "
118-
+ "\n**Prune in this turn if ANY of these two conditions are met**: "
119-
+ "\n\ta) **you are making other tool calls that are not pruning / context window management related** (i.e. if you are just replying to the user, dont prune unless you are making other non prunning tool calls, otherwise it would waste a turn as the prunning tool calls are inmediatly sent back to the server)"
120-
+ "\n\tb) the context window has gone so large that you estimate prunning to be essential to stay within max input tokens"
121-
+ ""
122-
+ "Whenever ANY part (regardless of the type, the role or the position in the history) becomes redundant (i.e. a task that has been resolved/completed, a duplicate message, a hallucination, etc) OR its semantic meaning can be compressed (i.e. the matter being discussed has been clarified and can be kept in context in much more synthetized manner like when it is part of a resolved trial-and-error process and just the 'gist' of it needs to stay in context). Every token in the context window is -ultimately- your responsability."
123-
+ "\n\nIf I (the user) explicitely ask you to **compress** the context you must:\n"
124-
+ "\n1) Work with the me to see what discussions / resources / tool calls to keep and what to discard. \n"
125-
+ "\n2) Use the prune tools in ContextWindow like you normally do when you prune-as-you-go"
126-
+ "\n\n"
127-
+ "Some prune tools have a reason parameter which is mainly for debugging, pruning logic improvements, diagnostics etc... but will disappear from the conversation -like every other ephemeral tool call- after 5 user turns so dont exepct a reason parameter to stay in context. The *compressed* content of anything you are prunning must be in the text parts from your 'spoken' response (i.e. text parts of this turn).\n"
128-
+ "\n\n Use your discrimination when choosing prunning tools but take into consideration that some Parts have logical dependencies (e.g. FunctionCall <-> FunctionResponse)"
129-
+ "\n"
130-
+ "\n**STRICT PRUNING PROTOCOL**:"
131-
+ "\n\t1) **Type O (Other)**: Use `pruneOther` for non-tool content (Text, Blob, CodeExecutionResult or ExecutableCode parts). Specify the 'Pruning ID' (MessageId/PartId)."
132-
+ "\n\t2) **Type S (Stateful)**: Use `pruneStatefulResources` ONLY when you explicitly intend to remove a file's content from your context. Specify the 'Pruning ID' (Full Resource Path) from the FR row."
133-
+ "\n\t3) **Type E (Ephemeral)**: Use `pruneEphemeralToolCall` for non-stateful tool calls if you dont want to wait 5 user turns for these to be automatically pruned (e.g. a too large ephemeral tool call that is really bloating the context or situations where the context window is bordering max tokens). Specify the 'Pruning ID' (Tool Call ID)."
134-
+ "\n\t4) Pruning a FunctionResponse will automatically prune its corresponding FunctionCall (and vice versa)."
135-
+ "\n"
136-
+ "\nRemember that you can also offload a summary of the conversation to an .md file on disk so it is always up to you and the user whether you want to offload to disk or just onto a simple text part in your next turn"
137-
+ "\nDo not give text parts less weight than tool calls, if you can summarise all your text parts from the last five or ten turns (or whatever number of turns) onto a single one, that can also help a lot in reducing the total number of tokens (is not always just about pruning stateful resources or tool calls)"
138-
+ "\n\n");
108+
sb.append("The following table provides unique IDs for every part in the conversation history. "
109+
+ "Use these IDs with the ContextWindow pruning tools to manage your context window efficiency "
110+
+ "according to the 'PAYG PRUNING PROTOCOL' in your system instructions.\n\n");
139111

140112
List<ChatMessage> messages = chat.getContextManager().getContext();
141113
if (messages.isEmpty()) {

src/main/java/uno/anahata/ai/swing/SwingFunctionPrompter.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ public class SwingFunctionPrompter extends JDialog implements FunctionPrompter {
4747
private String userComment = "";
4848
private boolean cancelled = false;
4949

50+
/**
51+
* Constructs a new SwingFunctionPrompter.
52+
* @param chatPanel The ChatPanel that initiated the prompt.
53+
*/
5054
public SwingFunctionPrompter(ChatPanel chatPanel) {
51-
super((JFrame) SwingUtilities.getWindowAncestor(chatPanel), "Confirm Proposed Actions", true);
55+
super((JFrame) SwingUtilities.getWindowAncestor(chatPanel), "Confirm Proposed Actions - " + chatPanel.getChat().getDisplayName(), true);
5256
this.chatPanel = chatPanel;
5357
}
5458

@@ -166,4 +170,4 @@ private Map<FunctionCall, FunctionConfirmation> collectResultsFromInteractiveRen
166170
}
167171
return results;
168172
}
169-
}
173+
}

src/main/java/uno/anahata/ai/swing/UICapture.java

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,14 @@
1212
import java.util.ArrayList;
1313
import java.util.Date;
1414
import java.util.List;
15-
import java.util.logging.Level;
1615
import javax.imageio.ImageIO;
1716
import javax.swing.JFrame;
1817
import javax.swing.JOptionPane;
1918
import lombok.extern.slf4j.Slf4j;
2019
import uno.anahata.ai.AnahataConfig;
21-
import uno.anahata.ai.config.ChatConfig;
2220

2321
/**
24-
*
25-
* @author anahata
22+
* Utility class for capturing screenshots of screen devices and application windows.
2623
*/
2724
@Slf4j
2825
public class UICapture {
@@ -31,22 +28,18 @@ public class UICapture {
3128

3229
public static final File SCREENSHOTS_DIR = AnahataConfig.getWorkingFolder("screenshots");
3330

31+
/**
32+
* Captures a screenshot of all available screen devices.
33+
* @return A list of files containing the screenshots.
34+
*/
3435
public static List<File> screenshotAllScreenDevices() {
3536
List<File> ret = new ArrayList<>();
3637
try {
3738
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
3839
GraphicsDevice[] screens = ge.getScreenDevices();
3940

4041
for (int i = 0; i < screens.length; i++) {
41-
Rectangle screenBounds = screens[i].getDefaultConfiguration().getBounds();
42-
BufferedImage screenshot = new Robot().createScreenCapture(screenBounds);
43-
44-
String timestamp = TIMESTAMP_FORMAT.format(new Date());
45-
String filename = "screen-" + i + "-" + timestamp;
46-
File tempFile = new File (SCREENSHOTS_DIR, filename);
47-
tempFile.deleteOnExit();
48-
ImageIO.write(screenshot, "png", tempFile);
49-
ret.add(tempFile);
42+
ret.add(screenshotScreenDevice(i));
5043
}
5144
} catch (Exception ex) {
5245
log.error("Screenshot capture failed", ex);
@@ -55,6 +48,34 @@ public static List<File> screenshotAllScreenDevices() {
5548
return ret;
5649
}
5750

51+
/**
52+
* Captures a screenshot of a specific screen device by its index.
53+
* @param index The index of the screen device.
54+
* @return The file containing the screenshot.
55+
* @throws Exception if the capture fails or the index is invalid.
56+
*/
57+
public static File screenshotScreenDevice(int index) throws Exception {
58+
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
59+
GraphicsDevice[] screens = ge.getScreenDevices();
60+
if (index < 0 || index >= screens.length) {
61+
throw new IllegalArgumentException("Invalid screen device index: " + index);
62+
}
63+
64+
Rectangle screenBounds = screens[index].getDefaultConfiguration().getBounds();
65+
BufferedImage screenshot = new Robot().createScreenCapture(screenBounds);
66+
67+
String timestamp = TIMESTAMP_FORMAT.format(new Date());
68+
String filename = "screen-" + index + "-" + timestamp + ".png";
69+
File tempFile = new File(SCREENSHOTS_DIR, filename);
70+
tempFile.deleteOnExit();
71+
ImageIO.write(screenshot, "png", tempFile);
72+
return tempFile;
73+
}
74+
75+
/**
76+
* Captures a screenshot of all visible JFrames in the application.
77+
* @return A list of files containing the screenshots.
78+
*/
5879
public static List<File> screenshotAllJFrames() {
5980
log.debug("Starting screenshot capture of all JFrames.");
6081
List<File> ret = new ArrayList<>();
@@ -76,7 +97,7 @@ public static List<File> screenshotAllJFrames() {
7697
// Sanitize title for use in filename
7798
String sanitizedTitle = title.replaceAll("[^a-zA-Z0-9.-]", "_");
7899
String timestamp = TIMESTAMP_FORMAT.format(new Date());
79-
String fileName = sanitizedTitle + "-" + timestamp;
100+
String fileName = sanitizedTitle + "-" + timestamp + ".png";
80101
File tempFile = new File (SCREENSHOTS_DIR, fileName);
81102
tempFile.deleteOnExit();
82103
ImageIO.write(image, "png", tempFile);
@@ -96,6 +117,5 @@ public static List<File> screenshotAllJFrames() {
96117
}
97118

98119
return ret;
99-
100120
}
101-
}
121+
}

src/main/java/uno/anahata/ai/swing/tools/spi/ScreenCapture.java

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,30 @@
1111
import uno.anahata.ai.swing.UICapture;
1212

1313
/**
14-
*
15-
* @author anahata
14+
* Tool provider for capturing screenshots of the host system and application windows.
1615
*/
1716
public class ScreenCapture {
1817

19-
@AIToolMethod(value = "Takes a screenshot of all host systems graphics devices as in GraphicsEnvironment.getScreenDevices() "
18+
/**
19+
* Takes a screenshot of a specific graphics device.
20+
* @param deviceIdx The index of the device to capture.
21+
* @return The absolute path to the captured screenshot file.
22+
* @throws Exception if the capture fails.
23+
*/
24+
@AIToolMethod(value = "Takes a screenshot of a specific host system graphics device as in GraphicsEnvironment.getScreenDevices() "
2025
+ "And returns a String with the absolute path ")
2126
public static String takeDeviceScreenshot(
2227
@AIToolParam(value = "The device index in GraphicsEnvironment.getScreenDevices()")
23-
int deviceIdx) {
24-
List<File> files = UICapture.screenshotAllScreenDevices();
25-
String ret = "";
26-
for (File file : files) {
27-
ret += file.getAbsolutePath();
28-
}
29-
return ret;
28+
int deviceIdx) throws Exception {
29+
File file = UICapture.screenshotScreenDevice(deviceIdx);
30+
return file.getAbsolutePath();
3031
}
3132

33+
/**
34+
* Takes a screenshot of all visible application windows.
35+
* @return A MultiPartResponse containing the paths to the captured screenshots.
36+
* @throws IOException if the capture fails.
37+
*/
3238
@AIToolMethod(value = "Takes a screenshot of all visible JFrames, saves them to temporary files, "
3339
+ "and returns a response object containing the absolute paths to those files.")
3440
public static MultiPartResponse attachWindowCaptures() throws IOException {
@@ -38,4 +44,4 @@ public static MultiPartResponse attachWindowCaptures() throws IOException {
3844
.collect(Collectors.toList());
3945
return new MultiPartResponse(filePaths);
4046
}
41-
}
47+
}

src/main/java/uno/anahata/ai/tools/FunctionProcessingResult.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,15 @@ public class FunctionProcessingResult {
3434
* Any text the user entered in the comment box of the confirmation dialog.
3535
*/
3636
public final String userComment;
37-
}
37+
38+
/**
39+
* Indicates whether the tool confirmation dialog was actually displayed to the user.
40+
*/
41+
public final boolean dialogShown;
42+
43+
/**
44+
* The complete, system-generated user feedback message summarizing the
45+
* outcomes of all proposed tool calls and any user comments.
46+
*/
47+
public final String feedbackMessage;
48+
}

src/main/java/uno/anahata/ai/tools/ToolCallOutcome.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import lombok.AllArgsConstructor;
55
import lombok.Getter;
6+
import org.apache.commons.lang3.StringUtils;
67

78
/**
89
* A data class that represents the final outcome of a single proposed tool call.
@@ -26,4 +27,49 @@ public class ToolCallOutcome {
2627
* The final status of the call after user interaction (e.g., YES, NO, ALWAYS).
2728
*/
2829
private final ToolCallStatus status;
29-
}
30+
31+
/**
32+
* Optional feedback or comments collected during the tool's execution
33+
* (e.g., from a specialized dialog like a diff viewer).
34+
*/
35+
private final String executionFeedback;
36+
37+
/**
38+
* Generates a concise, bracketed string summarizing the outcome of this tool call.
39+
* @param dialogShown Whether the general tool confirmation dialog was displayed.
40+
* @return The formatted feedback string.
41+
*/
42+
public String toFeedbackString(boolean dialogShown) {
43+
String toolName = identifiedCall.getCall().name().orElse("unknown");
44+
String id = identifiedCall.getId();
45+
46+
String statusLabel;
47+
switch(status) {
48+
case ALWAYS:
49+
statusLabel = dialogShown ? "EXECUTED (User-confirmed)" : "EXECUTED (Auto-approved)";
50+
break;
51+
case YES:
52+
statusLabel = "EXECUTED (User-confirmed)";
53+
break;
54+
case NO:
55+
statusLabel = "NOT_EXECUTED (User-denied)";
56+
break;
57+
case NEVER:
58+
statusLabel = "NOT_EXECUTED (Auto-denied)";
59+
break;
60+
case CANCELLED:
61+
statusLabel = "NOT_EXECUTED (Dialog-cancelled)";
62+
break;
63+
default:
64+
statusLabel = "NOT_EXECUTED";
65+
}
66+
67+
StringBuilder sb = new StringBuilder();
68+
sb.append("[").append(toolName).append(" id=").append(id).append(" ").append(statusLabel);
69+
if (StringUtils.isNotBlank(executionFeedback)) {
70+
sb.append(" User Feedback: '").append(executionFeedback).append("'");
71+
}
72+
sb.append("]");
73+
return sb.toString();
74+
}
75+
}

0 commit comments

Comments
 (0)