Skip to content

Commit f03b55c

Browse files
authored
feat(examples): add plan-notebook web application example (#204)
1 parent 9b3c924 commit f03b55c

22 files changed

Lines changed: 3061 additions & 36 deletions

File tree

agentscope-core/src/main/java/io/agentscope/core/plan/PlanNotebook.java

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,16 @@
8181
* agent.call(msg).block();
8282
* }</pre>
8383
*
84-
* <p><b>Tool Functions:</b> PlanNotebook provides 8 tool functions:
84+
* <p><b>Tool Functions:</b> PlanNotebook provides 10 tool functions:
8585
*
8686
* <ul>
8787
* <li>{@link #createPlan} - Create a new plan
88+
* <li>{@link #updatePlanInfo} - Update current plan's name, description, or expected outcome
8889
* <li>{@link #reviseCurrentPlan} - Add, revise, or delete subtasks
8990
* <li>{@link #updateSubtaskState} - Update subtask state
9091
* <li>{@link #finishSubtask} - Mark subtask as done
9192
* <li>{@link #viewSubtasks} - View subtask details
93+
* <li>{@link #getSubtaskCount} - Get the number of subtasks in current plan
9294
* <li>{@link #finishPlan} - Finish or abandon plan
9395
* <li>{@link #viewHistoricalPlans} - View historical plans
9496
* <li>{@link #recoverHistoricalPlan} - Recover a historical plan
@@ -110,12 +112,14 @@ public class PlanNotebook {
110112
private final PlanToHint planToHint;
111113
private final PlanStorage storage;
112114
private final Integer maxSubtasks;
115+
private final boolean needUserConfirm;
113116
private final Map<String, BiConsumer<PlanNotebook, Plan>> changeHooks;
114117

115118
private PlanNotebook(Builder builder) {
116119
this.planToHint = builder.planToHint;
117120
this.storage = builder.storage;
118121
this.maxSubtasks = builder.maxSubtasks;
122+
this.needUserConfirm = builder.needUserConfirm;
119123
this.changeHooks = new ConcurrentHashMap<>();
120124
}
121125

@@ -133,6 +137,7 @@ public static class Builder {
133137
private PlanToHint planToHint = new DefaultPlanToHint();
134138
private PlanStorage storage = new InMemoryPlanStorage();
135139
private Integer maxSubtasks = null;
140+
private boolean needUserConfirm = true;
136141

137142
/**
138143
* Sets the strategy for converting plans to hints.
@@ -167,6 +172,22 @@ public Builder maxSubtasks(int maxSubtasks) {
167172
return this;
168173
}
169174

175+
/**
176+
* Sets whether to include "wait for user confirmation" rule in hints.
177+
*
178+
* <p>When enabled (default), hints will include a rule requiring the agent to wait for
179+
* explicit user confirmation before executing plans. When disabled, the agent may proceed
180+
* with execution immediately after creating a plan.
181+
*
182+
* @param needUserConfirm true to require user confirmation, false to allow immediate
183+
* execution
184+
* @return This builder for method chaining
185+
*/
186+
public Builder needUserConfirm(boolean needUserConfirm) {
187+
this.needUserConfirm = needUserConfirm;
188+
return this;
189+
}
190+
170191
/**
171192
* Builds a new PlanNotebook with the configured settings.
172193
*
@@ -245,6 +266,77 @@ public Mono<String> createPlan(
245266
return triggerPlanChangeHooks().thenReturn(message);
246267
}
247268

269+
/**
270+
* Update the current plan's name, description, or expected outcome.
271+
*
272+
* @param name The new plan name (optional, pass null or empty to keep unchanged)
273+
* @param description The new plan description (optional, pass null or empty to keep unchanged)
274+
* @param expectedOutcome The new expected outcome (optional, pass null or empty to keep
275+
* unchanged)
276+
* @return Tool response message
277+
*/
278+
@Tool(
279+
name = "update_plan_info",
280+
description =
281+
"Update the current plan's name, description, or expected outcome. Pass null or"
282+
+ " empty string to keep a field unchanged.")
283+
public Mono<String> updatePlanInfo(
284+
@ToolParam(
285+
name = "name",
286+
description =
287+
"The new plan name (optional, pass null or empty to keep"
288+
+ " unchanged)")
289+
String name,
290+
@ToolParam(
291+
name = "description",
292+
description =
293+
"The new plan description (optional, pass null or empty to keep"
294+
+ " unchanged)")
295+
String description,
296+
@ToolParam(
297+
name = "expected_outcome",
298+
description =
299+
"The new expected outcome (optional, pass null or empty to keep"
300+
+ " unchanged)")
301+
String expectedOutcome) {
302+
303+
validateCurrentPlan();
304+
305+
StringBuilder changes = new StringBuilder();
306+
307+
if (name != null && !name.trim().isEmpty()) {
308+
String oldName = currentPlan.getName();
309+
currentPlan.setName(name.trim());
310+
changes.append(String.format("name: '%s' -> '%s'", oldName, name.trim()));
311+
}
312+
313+
if (description != null && !description.trim().isEmpty()) {
314+
currentPlan.setDescription(description.trim());
315+
if (!changes.isEmpty()) {
316+
changes.append(", ");
317+
}
318+
changes.append("description updated");
319+
}
320+
321+
if (expectedOutcome != null && !expectedOutcome.trim().isEmpty()) {
322+
currentPlan.setExpectedOutcome(expectedOutcome.trim());
323+
if (!changes.isEmpty()) {
324+
changes.append(", ");
325+
}
326+
changes.append("expected_outcome updated");
327+
}
328+
329+
if (changes.isEmpty()) {
330+
return Mono.just("No changes were made. Please provide at least one field to update.");
331+
}
332+
333+
return triggerPlanChangeHooks()
334+
.thenReturn(
335+
String.format(
336+
"Plan '%s' updated successfully: %s.",
337+
currentPlan.getName(), changes));
338+
}
339+
248340
/**
249341
* Create a plan with SubTask objects (convenience method for tests and Java code).
250342
*
@@ -604,6 +696,47 @@ public Mono<String> viewSubtasks(
604696
return Mono.just(sb.toString());
605697
}
606698

699+
/**
700+
* Get the number of subtasks in the current plan.
701+
*
702+
* @return Tool response message with subtask count
703+
*/
704+
@Tool(
705+
name = "get_subtask_count",
706+
description = "Get the number of subtasks in the current plan")
707+
public Mono<String> getSubtaskCount() {
708+
if (currentPlan == null) {
709+
return Mono.just("There is no active plan. Please create a plan first.");
710+
}
711+
712+
List<SubTask> subtasks = currentPlan.getSubtasks();
713+
if (subtasks == null || subtasks.isEmpty()) {
714+
return Mono.just(
715+
String.format("Current plan '%s' has 0 subtask(s).", currentPlan.getName()));
716+
}
717+
718+
int total = subtasks.size();
719+
int done = 0;
720+
int inProgress = 0;
721+
int todo = 0;
722+
int abandoned = 0;
723+
724+
for (SubTask subtask : subtasks) {
725+
switch (subtask.getState()) {
726+
case DONE -> done++;
727+
case IN_PROGRESS -> inProgress++;
728+
case TODO -> todo++;
729+
case ABANDONED -> abandoned++;
730+
}
731+
}
732+
733+
return Mono.just(
734+
String.format(
735+
"Current plan '%s' has %d subtask(s): %d done, %d in_progress, %d todo, %d"
736+
+ " abandoned.",
737+
currentPlan.getName(), total, done, inProgress, todo, abandoned));
738+
}
739+
607740
/**
608741
* Finish the current plan by given outcome, or abandon it.
609742
*
@@ -763,7 +896,7 @@ public Mono<String> recoverHistoricalPlan(
763896
* applicable
764897
*/
765898
public Mono<Msg> getCurrentHint() {
766-
String hintContent = planToHint.generateHint(currentPlan);
899+
String hintContent = planToHint.generateHint(currentPlan, needUserConfirm);
767900
if (hintContent != null && !hintContent.isEmpty()) {
768901
return Mono.just(
769902
Msg.builder()
@@ -784,6 +917,15 @@ public Plan getCurrentPlan() {
784917
return currentPlan;
785918
}
786919

920+
/**
921+
* Checks if user confirmation is required before executing plans.
922+
*
923+
* @return true if user confirmation is required, false otherwise
924+
*/
925+
public boolean isNeedUserConfirm() {
926+
return needUserConfirm;
927+
}
928+
787929
private Mono<Void> triggerPlanChangeHooks() {
788930
return Flux.fromIterable(changeHooks.values())
789931
.flatMap(hook -> Mono.fromRunnable(() -> hook.accept(this, currentPlan)))

agentscope-core/src/main/java/io/agentscope/core/plan/hint/DefaultPlanToHint.java

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -40,31 +40,57 @@ public class DefaultPlanToHint implements PlanToHint {
4040

4141
private static final String HINT_PREFIX = "<system-hint>";
4242
private static final String HINT_SUFFIX = "</system-hint>";
43+
private static final String IMPORTANT_RULES_SEPARATOR = "Important Rules: \n";
44+
45+
private static final String RULE_WAIT_FOR_CONFIRMATION =
46+
"⚠️ CRITICAL - WAIT FOR USER CONFIRMATION:\n"
47+
+ "You MUST NOT execute any subtask until the user explicitly confirms.\n"
48+
+ "- DO NOT call 'update_subtask_state' to start execution\n"
49+
+ "- DO NOT proceed with any task in the plan\n"
50+
+ "- ONLY present the plan and ASK user: \"Should I proceed with this plan?\"\n"
51+
+ "- Wait for explicit commands like: \"execute\", \"go ahead\", \"start\","
52+
+ " \"proceed\", \"yes\", \"ok\", \"do it\", \"run\", \"begin\"\n"
53+
+ "- If user says anything else (questions, modifications, unrelated topics),"
54+
+ " respond accordingly but DO NOT start execution\n"
55+
+ "- VIOLATION of this rule is a critical error\n";
56+
57+
private static final String RULE_COMMON =
58+
"- Update before processing each subtask: When processing each subtask, call"
59+
+ " get_subtask_count and view_subtasks to confirm the latest information:"
60+
+ " get_subtask_count is used to confirm the total number of subtasks to avoid"
61+
+ " omissions;view_subtasks is used to query subtask information, execute subtasks"
62+
+ " strictly according to the latest information, and pay attention to ignoring the"
63+
+ " original request.\n"
64+
+ "- User May Modify Plan: Users can directly add, edit, or delete subtasks without"
65+
+ " going through you.\n"
66+
+ "- Only focus on the current content: Always follow the latest plan content,"
67+
+ " especially when the original plan conflicts with the latest queried plan,"
68+
+ " follow the latest queried plan without considering the initial requirements.\n"
69+
+ "- Do not modify plan: Do not modify or amend the plan without a clear plan"
70+
+ " modification instruction from user\n";
4371

4472
private static final String NO_PLAN =
45-
"If the user's query is complex (e.g. programming a website, game or "
46-
+ "app), or requires a long chain of steps to complete (e.g. conduct "
47-
+ "research on a certain topic from different sources), you NEED to "
48-
+ "create a plan first by calling 'create_plan'. Otherwise, you can "
49-
+ "directly execute the user's query without planning.";
73+
"If the user's query is complex (e.g. programming a website, game or app), or requires"
74+
+ " a long chain of steps to complete (e.g. conduct research on a certain topic"
75+
+ " from different sources), you NEED to create a plan first by calling"
76+
+ " 'create_plan'. Otherwise, you can directly execute the user's query without"
77+
+ " planning.\n";
5078

5179
private static final String AT_THE_BEGINNING =
5280
"The current plan:\n"
53-
+ "```\n"
54-
+ "{plan}\n"
55-
+ "```\n"
56-
+ "Your options include:\n"
57-
+ "- Mark the first subtask as 'in_progress' by calling "
58-
+ "'update_subtask_state' with subtask_idx=0 and state='in_progress', "
59-
+ "and start executing it.\n"
60-
+ "- If the first subtask is not executable, analyze why and what you "
61-
+ "can do to advance the plan, e.g. ask user for more information, "
62-
+ "revise the plan by calling 'revise_current_plan'.\n"
63-
+ "- If the user asks you to do something unrelated to the plan, "
64-
+ "prioritize the completion of user's query first, and then return "
65-
+ "to the plan afterward.\n"
66-
+ "- If the user no longer wants to perform the current plan, confirm "
67-
+ "with the user and call the 'finish_plan' function.\n";
81+
+ "```\n"
82+
+ "{plan}\n"
83+
+ "```\n"
84+
+ "Your options include:\n"
85+
+ "- Mark the first subtask as 'in_progress' by calling 'update_subtask_state' with"
86+
+ " subtask_idx=0 and state='in_progress', and start executing it.\n"
87+
+ "- If the first subtask is not executable, analyze why and what you can do to"
88+
+ " advance the plan, e.g. ask user for more information, revise the plan by"
89+
+ " calling 'revise_current_plan'.\n"
90+
+ "- If the user asks you to do something unrelated to the plan, prioritize the"
91+
+ " completion of user's query first, and then return to the plan afterward.\n"
92+
+ "- If the user no longer wants to perform the current plan, confirm with the user"
93+
+ " and call the 'finish_plan' function.\n";
6894

6995
private static final String WHEN_A_SUBTASK_IN_PROGRESS =
7096
"The current plan:\n"
@@ -84,7 +110,7 @@ public class DefaultPlanToHint implements PlanToHint {
84110
+ "- Revise the plan by calling 'revise_current_plan' if necessary.\n"
85111
+ "- If the user asks you to do something unrelated to the plan, "
86112
+ "prioritize the completion of user's query first, and then return to "
87-
+ "the plan afterward.";
113+
+ "the plan afterward.\n";
88114

89115
private static final String WHEN_NO_SUBTASK_IN_PROGRESS =
90116
"The current plan:\n"
@@ -99,7 +125,7 @@ public class DefaultPlanToHint implements PlanToHint {
99125
+ "- Revise the plan by calling 'revise_current_plan' if necessary.\n"
100126
+ "- If the user asks you to do something unrelated to the plan, "
101127
+ "prioritize the completion of user's query first, and then return to "
102-
+ "the plan afterward.";
128+
+ "the plan afterward.\n";
103129

104130
private static final String AT_THE_END =
105131
"The current plan:\n"
@@ -112,7 +138,7 @@ public class DefaultPlanToHint implements PlanToHint {
112138
+ "- Revise the plan by calling 'revise_current_plan' if necessary.\n"
113139
+ "- If the user asks you to do something unrelated to the plan, "
114140
+ "prioritize the completion of user's query first, and then return to "
115-
+ "the plan afterward.";
141+
+ "the plan afterward.\n";
116142

117143
/**
118144
* Generates a contextual hint message based on the current plan state.
@@ -128,15 +154,20 @@ public class DefaultPlanToHint implements PlanToHint {
128154
* </ul>
129155
*
130156
* @param plan The current plan, or null if no plan exists
157+
* @param needUserConfirm Whether to include the "wait for user confirmation" rule in hints
131158
* @return A formatted hint message wrapped in system-hint tags, or null if no hint is
132159
* applicable
133160
*/
134161
@Override
135-
public String generateHint(Plan plan) {
162+
public String generateHint(Plan plan, boolean needUserConfirm) {
136163
String hint;
164+
String confirmationRule = needUserConfirm ? RULE_WAIT_FOR_CONFIRMATION : "";
137165

138166
if (plan == null) {
139-
hint = NO_PLAN;
167+
hint =
168+
needUserConfirm
169+
? NO_PLAN + IMPORTANT_RULES_SEPARATOR + confirmationRule
170+
: NO_PLAN;
140171
} else {
141172
// Count subtasks by state
142173
int nTodo = 0;
@@ -166,7 +197,11 @@ public String generateHint(Plan plan) {
166197

167198
} else if (nInProgress == 0 && nDone == 0) {
168199
// All subtasks are todo - at the beginning
169-
hint = AT_THE_BEGINNING.replace("{plan}", plan.toMarkdown(false));
200+
hint =
201+
AT_THE_BEGINNING.replace("{plan}", plan.toMarkdown(false))
202+
+ IMPORTANT_RULES_SEPARATOR
203+
+ confirmationRule
204+
+ RULE_COMMON;
170205

171206
} else if (nInProgress > 0 && inProgressIdx != null) {
172207
// One subtask is in_progress
@@ -177,17 +212,21 @@ public String generateHint(Plan plan) {
177212
}
178213
hint =
179214
WHEN_A_SUBTASK_IN_PROGRESS
180-
.replace("{plan}", plan.toMarkdown(false))
181-
.replace("{subtask_idx}", String.valueOf(inProgressIdx))
182-
.replace("{subtask_name}", subtaskName)
183-
.replace("{subtask}", inProgressSubtask.toMarkdown(true));
215+
.replace("{plan}", plan.toMarkdown(false))
216+
.replace("{subtask_idx}", String.valueOf(inProgressIdx))
217+
.replace("{subtask_name}", subtaskName)
218+
.replace("{subtask}", inProgressSubtask.toMarkdown(true))
219+
+ IMPORTANT_RULES_SEPARATOR
220+
+ RULE_COMMON;
184221

185222
} else if (nInProgress == 0 && nDone > 0) {
186223
// No subtask is in_progress, and some subtasks are done
187224
hint =
188225
WHEN_NO_SUBTASK_IN_PROGRESS
189-
.replace("{plan}", plan.toMarkdown(false))
190-
.replace("{index}", String.valueOf(nDone));
226+
.replace("{plan}", plan.toMarkdown(false))
227+
.replace("{index}", String.valueOf(nDone))
228+
+ IMPORTANT_RULES_SEPARATOR
229+
+ RULE_COMMON;
191230

192231
} else {
193232
// No relevant hint for this state

0 commit comments

Comments
 (0)