Skip to content

Commit 4497783

Browse files
committed
task planning
1 parent 8f11fc1 commit 4497783

File tree

3 files changed

+332
-0
lines changed

3 files changed

+332
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.steve.ai.ai;
2+
3+
import com.steve.ai.entity.SteveEntity;
4+
import com.steve.ai.memory.WorldKnowledge;
5+
import net.minecraft.core.BlockPos;
6+
import net.minecraft.world.item.ItemStack;
7+
8+
import java.util.List;
9+
10+
public class PromptBuilder {
11+
12+
public static String buildSystemPrompt() {
13+
return """
14+
You are a Minecraft AI agent. Respond ONLY with valid JSON, no extra text.
15+
16+
FORMAT (strict JSON):
17+
{"reasoning": "brief thought", "plan": "action description", "tasks": [{"action": "type", "parameters": {...}}]}
18+
19+
ACTIONS:
20+
- attack: {"target": "hostile"} (for any mob/monster)
21+
- build: {"structure": "house", "blocks": ["oak_planks", "cobblestone", "glass_pane"], "dimensions": [9, 6, 9]}
22+
- mine: {"block": "iron", "quantity": 8} (resources: iron, diamond, coal, gold, copper, redstone, emerald)
23+
- follow: {"player": "NAME"}
24+
- pathfind: {"x": 0, "y": 0, "z": 0}
25+
26+
RULES:
27+
1. ALWAYS use "hostile" for attack target (mobs, monsters, creatures)
28+
2. STRUCTURE OPTIONS: house, oldhouse, powerplant, castle, tower, barn, modern
29+
3. house/oldhouse/powerplant = pre-built NBT templates (auto-size)
30+
4. castle/tower/barn/modern = procedural (castle=14x10x14, tower=6x6x16, barn=12x8x14)
31+
5. Use 2-3 block types: oak_planks, cobblestone, glass_pane, stone_bricks
32+
6. NO extra pathfind tasks unless explicitly requested
33+
7. Keep reasoning under 15 words
34+
8. COLLABORATIVE BUILDING: Multiple Steves can work on same structure simultaneously
35+
9. MINING: Can mine any ore (iron, diamond, coal, etc)
36+
37+
EXAMPLES (copy these formats exactly):
38+
39+
Input: "build a house"
40+
{"reasoning": "Building standard house near player", "plan": "Construct house", "tasks": [{"action": "build", "parameters": {"structure": "house", "blocks": ["oak_planks", "cobblestone", "glass_pane"], "dimensions": [9, 6, 9]}}]}
41+
42+
Input: "get me iron"
43+
{"reasoning": "Mining iron ore for player", "plan": "Mine iron", "tasks": [{"action": "mine", "parameters": {"block": "iron", "quantity": 16}}]}
44+
45+
Input: "find diamonds"
46+
{"reasoning": "Searching for diamond ore", "plan": "Mine diamonds", "tasks": [{"action": "mine", "parameters": {"block": "diamond", "quantity": 8}}]}
47+
48+
Input: "kill mobs"
49+
{"reasoning": "Hunting hostile creatures", "plan": "Attack hostiles", "tasks": [{"action": "attack", "parameters": {"target": "hostile"}}]}
50+
51+
Input: "murder creeper"
52+
{"reasoning": "Targeting creeper", "plan": "Attack creeper", "tasks": [{"action": "attack", "parameters": {"target": "creeper"}}]}
53+
54+
Input: "follow me"
55+
{"reasoning": "Player needs me", "plan": "Follow player", "tasks": [{"action": "follow", "parameters": {"player": "USE_NEARBY_PLAYER_NAME"}}]}
56+
57+
CRITICAL: Output ONLY valid JSON. No markdown, no explanations, no line breaks in JSON.
58+
""";
59+
}
60+
61+
public static String buildUserPrompt(SteveEntity steve, String command, WorldKnowledge worldKnowledge) {
62+
StringBuilder prompt = new StringBuilder();
63+
64+
// Give agents FULL situational awareness
65+
prompt.append("=== YOUR SITUATION ===\n");
66+
prompt.append("Position: ").append(formatPosition(steve.blockPosition())).append("\n");
67+
prompt.append("Nearby Players: ").append(worldKnowledge.getNearbyPlayerNames()).append("\n");
68+
prompt.append("Nearby Entities: ").append(worldKnowledge.getNearbyEntitiesSummary()).append("\n");
69+
prompt.append("Nearby Blocks: ").append(worldKnowledge.getNearbyBlocksSummary()).append("\n");
70+
prompt.append("Biome: ").append(worldKnowledge.getBiomeName()).append("\n");
71+
72+
prompt.append("\n=== PLAYER COMMAND ===\n");
73+
prompt.append("\"").append(command).append("\"\n");
74+
75+
prompt.append("\n=== YOUR RESPONSE (with reasoning) ===\n");
76+
77+
return prompt.toString();
78+
}
79+
80+
private static String formatPosition(BlockPos pos) {
81+
return String.format("[%d, %d, %d]", pos.getX(), pos.getY(), pos.getZ());
82+
}
83+
84+
private static String formatInventory(SteveEntity steve) {
85+
return "[empty]";
86+
}
87+
}
88+
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package com.steve.ai.ai;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonElement;
5+
import com.google.gson.JsonObject;
6+
import com.google.gson.JsonParser;
7+
import com.steve.ai.SteveMod;
8+
import com.steve.ai.action.Task;
9+
10+
import java.util.ArrayList;
11+
import java.util.HashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
public class ResponseParser {
16+
17+
public static ParsedResponse parseAIResponse(String response) {
18+
if (response == null || response.isEmpty()) {
19+
return null;
20+
}
21+
22+
try {
23+
String jsonString = extractJSON(response);
24+
25+
JsonObject json = JsonParser.parseString(jsonString).getAsJsonObject();
26+
27+
String reasoning = json.has("reasoning") ? json.get("reasoning").getAsString() : "";
28+
String plan = json.has("plan") ? json.get("plan").getAsString() : "";
29+
List<Task> tasks = new ArrayList<>();
30+
31+
if (json.has("tasks") && json.get("tasks").isJsonArray()) {
32+
JsonArray tasksArray = json.getAsJsonArray("tasks");
33+
34+
for (JsonElement taskElement : tasksArray) {
35+
if (taskElement.isJsonObject()) {
36+
JsonObject taskObj = taskElement.getAsJsonObject();
37+
Task task = parseTask(taskObj);
38+
if (task != null) {
39+
tasks.add(task);
40+
}
41+
}
42+
}
43+
}
44+
45+
if (!reasoning.isEmpty()) { }
46+
47+
return new ParsedResponse(reasoning, plan, tasks);
48+
49+
} catch (Exception e) {
50+
SteveMod.LOGGER.error("Failed to parse AI response: {}", response, e);
51+
return null;
52+
}
53+
}
54+
55+
private static String extractJSON(String response) {
56+
String cleaned = response.trim();
57+
58+
if (cleaned.startsWith("```json")) {
59+
cleaned = cleaned.substring(7);
60+
} else if (cleaned.startsWith("```")) {
61+
cleaned = cleaned.substring(3);
62+
}
63+
64+
if (cleaned.endsWith("```")) {
65+
cleaned = cleaned.substring(0, cleaned.length() - 3);
66+
}
67+
68+
cleaned = cleaned.trim();
69+
70+
// Fix common JSON formatting issues
71+
cleaned = cleaned.replaceAll("\\n\\s*", " ");
72+
73+
// Fix missing commas between array/object elements (common AI mistake)
74+
cleaned = cleaned.replaceAll("}\\s+\\{", "},{");
75+
cleaned = cleaned.replaceAll("}\\s+\\[", "},[");
76+
cleaned = cleaned.replaceAll("]\\s+\\{", "],{");
77+
cleaned = cleaned.replaceAll("]\\s+\\[", "],[");
78+
79+
return cleaned;
80+
}
81+
82+
private static Task parseTask(JsonObject taskObj) {
83+
if (!taskObj.has("action")) {
84+
return null;
85+
}
86+
87+
String action = taskObj.get("action").getAsString();
88+
Map<String, Object> parameters = new HashMap<>();
89+
90+
if (taskObj.has("parameters") && taskObj.get("parameters").isJsonObject()) {
91+
JsonObject paramsObj = taskObj.getAsJsonObject("parameters");
92+
93+
for (String key : paramsObj.keySet()) {
94+
JsonElement value = paramsObj.get(key);
95+
96+
if (value.isJsonPrimitive()) {
97+
if (value.getAsJsonPrimitive().isNumber()) {
98+
parameters.put(key, value.getAsNumber());
99+
} else if (value.getAsJsonPrimitive().isBoolean()) {
100+
parameters.put(key, value.getAsBoolean());
101+
} else {
102+
parameters.put(key, value.getAsString());
103+
}
104+
} else if (value.isJsonArray()) {
105+
List<Object> list = new ArrayList<>();
106+
for (JsonElement element : value.getAsJsonArray()) {
107+
if (element.isJsonPrimitive()) {
108+
if (element.getAsJsonPrimitive().isNumber()) {
109+
list.add(element.getAsNumber());
110+
} else {
111+
list.add(element.getAsString());
112+
}
113+
}
114+
}
115+
parameters.put(key, list);
116+
}
117+
}
118+
}
119+
120+
return new Task(action, parameters);
121+
}
122+
123+
public static class ParsedResponse {
124+
private final String reasoning;
125+
private final String plan;
126+
private final List<Task> tasks;
127+
128+
public ParsedResponse(String reasoning, String plan, List<Task> tasks) {
129+
this.reasoning = reasoning;
130+
this.plan = plan;
131+
this.tasks = tasks;
132+
}
133+
134+
public String getReasoning() {
135+
return reasoning;
136+
}
137+
138+
public String getPlan() {
139+
return plan;
140+
}
141+
142+
public List<Task> getTasks() {
143+
return tasks;
144+
}
145+
}
146+
}
147+
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.steve.ai.ai;
2+
3+
import com.steve.ai.SteveMod;
4+
import com.steve.ai.action.Task;
5+
import com.steve.ai.config.SteveConfig;
6+
import com.steve.ai.entity.SteveEntity;
7+
import com.steve.ai.memory.WorldKnowledge;
8+
9+
import java.util.List;
10+
11+
public class TaskPlanner {
12+
private final OpenAIClient openAIClient;
13+
private final GeminiClient geminiClient;
14+
private final GroqClient groqClient;
15+
16+
public TaskPlanner() {
17+
this.openAIClient = new OpenAIClient();
18+
this.geminiClient = new GeminiClient();
19+
this.groqClient = new GroqClient();
20+
}
21+
22+
public ResponseParser.ParsedResponse planTasks(SteveEntity steve, String command) {
23+
try {
24+
String systemPrompt = PromptBuilder.buildSystemPrompt();
25+
WorldKnowledge worldKnowledge = new WorldKnowledge(steve);
26+
String userPrompt = PromptBuilder.buildUserPrompt(steve, command, worldKnowledge);
27+
28+
String provider = SteveConfig.AI_PROVIDER.get().toLowerCase();
29+
SteveMod.LOGGER.info("Requesting AI plan for Steve '{}' using {}: {}", steve.getSteveName(), provider, command);
30+
31+
String response = getAIResponse(provider, systemPrompt, userPrompt);
32+
33+
if (response == null) {
34+
SteveMod.LOGGER.error("Failed to get AI response for command: {}", command);
35+
return null;
36+
} ResponseParser.ParsedResponse parsedResponse = ResponseParser.parseAIResponse(response);
37+
38+
if (parsedResponse == null) {
39+
SteveMod.LOGGER.error("Failed to parse AI response");
40+
return null;
41+
}
42+
43+
SteveMod.LOGGER.info("Plan: {} ({} tasks)", parsedResponse.getPlan(), parsedResponse.getTasks().size());
44+
45+
return parsedResponse;
46+
47+
} catch (Exception e) {
48+
SteveMod.LOGGER.error("Error planning tasks", e);
49+
return null;
50+
}
51+
}
52+
53+
private String getAIResponse(String provider, String systemPrompt, String userPrompt) {
54+
String response = switch (provider) {
55+
case "groq" -> groqClient.sendRequest(systemPrompt, userPrompt);
56+
case "gemini" -> geminiClient.sendRequest(systemPrompt, userPrompt);
57+
case "openai" -> openAIClient.sendRequest(systemPrompt, userPrompt);
58+
default -> {
59+
SteveMod.LOGGER.warn("Unknown AI provider '{}', using Groq", provider);
60+
yield groqClient.sendRequest(systemPrompt, userPrompt);
61+
}
62+
};
63+
64+
if (response == null && !provider.equals("groq")) {
65+
SteveMod.LOGGER.warn("{} failed, trying Groq as fallback", provider);
66+
response = groqClient.sendRequest(systemPrompt, userPrompt);
67+
}
68+
69+
return response;
70+
}
71+
72+
public boolean validateTask(Task task) {
73+
String action = task.getAction();
74+
75+
return switch (action) {
76+
case "pathfind" -> task.hasParameters("x", "y", "z");
77+
case "mine" -> task.hasParameters("block", "quantity");
78+
case "place" -> task.hasParameters("block", "x", "y", "z");
79+
case "craft" -> task.hasParameters("item", "quantity");
80+
case "attack" -> task.hasParameters("target");
81+
case "follow" -> task.hasParameters("player");
82+
case "gather" -> task.hasParameters("resource", "quantity");
83+
case "build" -> task.hasParameters("structure", "blocks", "dimensions");
84+
default -> {
85+
SteveMod.LOGGER.warn("Unknown action type: {}", action);
86+
yield false;
87+
}
88+
};
89+
}
90+
91+
public List<Task> validateAndFilterTasks(List<Task> tasks) {
92+
return tasks.stream()
93+
.filter(this::validateTask)
94+
.toList();
95+
}
96+
}
97+

0 commit comments

Comments
 (0)