Skip to content

Commit 5999e0f

Browse files
committed
[agents] Add GOAP planner test
1 parent 3526d03 commit 5999e0f

File tree

1 file changed

+254
-0
lines changed

1 file changed

+254
-0
lines changed
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package ai.koog.agents.planner
2+
3+
import ai.koog.agents.core.agent.config.AIAgentConfig
4+
import ai.koog.agents.planner.goap.goap
5+
import ai.koog.agents.testing.tools.getMockExecutor
6+
import ai.koog.prompt.dsl.prompt
7+
import ai.koog.prompt.llm.OllamaModels
8+
import kotlinx.coroutines.test.runTest
9+
import kotlin.reflect.typeOf
10+
import kotlin.test.Test
11+
import kotlin.test.assertTrue
12+
13+
class GOAPPlannerAgentTest {
14+
// Create a GOAP planner with a simple linear action sequence
15+
16+
data class SimpleState(
17+
val hasKey: Boolean = false,
18+
val doorUnlocked: Boolean = false,
19+
val treasureFound: Boolean = false
20+
)
21+
22+
@Test
23+
fun testGOAPLinearPath() = runTest {
24+
val planner = goap<SimpleState>(typeOf<SimpleState>()) {
25+
// Action to get the key
26+
action(
27+
name = "Get key",
28+
precondition = { state -> !state.hasKey },
29+
belief = { state -> state.copy(hasKey = true) },
30+
cost = { 1.0 }
31+
) { _, state ->
32+
state.copy(hasKey = true)
33+
}
34+
35+
// Action to unlock door (requires key)
36+
action(
37+
name = "Unlock door",
38+
precondition = { state -> state.hasKey && !state.doorUnlocked },
39+
belief = { state -> state.copy(doorUnlocked = true) },
40+
cost = { 1.0 }
41+
) { _, state ->
42+
state.copy(doorUnlocked = true)
43+
}
44+
45+
// Action to find treasure (requires unlocked door)
46+
action(
47+
name = "Find treasure",
48+
precondition = { state -> state.doorUnlocked && !state.treasureFound },
49+
belief = { state -> state.copy(treasureFound = true) },
50+
cost = { 1.0 }
51+
) { _, state ->
52+
state.copy(treasureFound = true)
53+
}
54+
55+
// Goal: find the treasure
56+
goal(
57+
name = "Find treasure",
58+
condition = { state -> state.treasureFound }
59+
)
60+
}
61+
62+
val strategy = AIAgentPlannerStrategy("goap-linear-test", planner)
63+
val mockExecutor = getMockExecutor {
64+
mockLLMAnswer("OK").asDefaultResponse
65+
}
66+
67+
val agentConfig = AIAgentConfig(
68+
prompt = prompt("goap") { system("GOAP agent") },
69+
model = OllamaModels.Meta.LLAMA_3_2,
70+
maxAgentIterations = 50
71+
)
72+
73+
val agent = PlannerAIAgent(
74+
promptExecutor = mockExecutor,
75+
strategy = strategy,
76+
agentConfig = agentConfig
77+
)
78+
79+
val initialState = SimpleState()
80+
val finalState = agent.run(initialState)
81+
82+
assertTrue(finalState.hasKey, "Agent should have obtained the key")
83+
assertTrue(finalState.doorUnlocked, "Agent should have unlocked the door")
84+
assertTrue(finalState.treasureFound, "Agent should have found the treasure")
85+
}
86+
87+
// Create a GOAP planner with multiple paths to the goal
88+
// One path is more expensive than the other
89+
90+
data class PathState(
91+
val hasItem: Boolean = false,
92+
val goalReached: Boolean = false
93+
)
94+
95+
@Test
96+
fun testGOAPOptimalPathSelection() = runTest {
97+
val planner = goap<PathState>(typeOf<PathState>()) {
98+
// Expensive path: cost 10
99+
action(
100+
name = "Expensive route",
101+
precondition = { state -> !state.hasItem },
102+
belief = { state -> state.copy(hasItem = true) },
103+
cost = { 10.0 }
104+
) { _, _ ->
105+
throw IllegalStateException("Expensive route should not be selected")
106+
}
107+
108+
// Cheap path: cost 1
109+
action(
110+
name = "Cheap route",
111+
precondition = { state -> !state.hasItem },
112+
belief = { state -> state.copy(hasItem = true) },
113+
cost = { 1.0 }
114+
) { _, state ->
115+
state.copy(hasItem = true)
116+
}
117+
118+
// Final action
119+
action(
120+
name = "Reach goal",
121+
precondition = { state -> state.hasItem && !state.goalReached },
122+
belief = { state -> state.copy(goalReached = true) },
123+
cost = { 1.0 }
124+
) { _, state ->
125+
state.copy(goalReached = true)
126+
}
127+
128+
goal(
129+
name = "Reach goal",
130+
condition = { state -> state.goalReached }
131+
)
132+
}
133+
134+
val strategy = AIAgentPlannerStrategy("goap-optimal-path-test", planner)
135+
val mockExecutor = getMockExecutor {
136+
mockLLMAnswer("OK").asDefaultResponse
137+
}
138+
139+
val agentConfig = AIAgentConfig(
140+
prompt = prompt("goap") { system("GOAP agent") },
141+
model = OllamaModels.Meta.LLAMA_3_2,
142+
maxAgentIterations = 50
143+
)
144+
145+
val agent = PlannerAIAgent(
146+
promptExecutor = mockExecutor,
147+
strategy = strategy,
148+
agentConfig = agentConfig
149+
)
150+
151+
val initialState = PathState()
152+
val finalState = agent.run(initialState)
153+
154+
assertTrue(finalState.hasItem, "Agent should have obtained the item")
155+
assertTrue(finalState.goalReached, "Agent should have reached the goal")
156+
}
157+
158+
// Create a more complex scenario with multiple dependencies
159+
data class ComplexState(
160+
val hasWood: Boolean = false,
161+
val hasStone: Boolean = false,
162+
val hasAxe: Boolean = false,
163+
val hasPickaxe: Boolean = false,
164+
val hasShelter: Boolean = false
165+
)
166+
167+
@Test
168+
fun testGOAPComplexDependencies() = runTest {
169+
val planner = goap<ComplexState>(typeOf<ComplexState>()) {
170+
// Gather wood (no prerequisites)
171+
action(
172+
name = "Gather wood",
173+
precondition = { state -> !state.hasWood },
174+
belief = { state -> state.copy(hasWood = true) },
175+
cost = { 2.0 }
176+
) { _, state ->
177+
state.copy(hasWood = true)
178+
}
179+
180+
// Gather stone (no prerequisites)
181+
action(
182+
name = "Gather stone",
183+
precondition = { state -> !state.hasStone },
184+
belief = { state -> state.copy(hasStone = true) },
185+
cost = { 2.0 }
186+
) { _, state ->
187+
state.copy(hasStone = true)
188+
}
189+
190+
// Craft axe (requires wood and stone)
191+
action(
192+
name = "Craft axe",
193+
precondition = { state -> state.hasWood && state.hasStone && !state.hasAxe },
194+
belief = { state -> state.copy(hasAxe = true) },
195+
cost = { 1.0 }
196+
) { _, state ->
197+
state.copy(hasAxe = true)
198+
}
199+
200+
// Craft pickaxe (requires wood and stone)
201+
action(
202+
name = "Craft pickaxe",
203+
precondition = { state -> state.hasWood && state.hasStone && !state.hasPickaxe },
204+
belief = { state -> state.copy(hasPickaxe = true) },
205+
cost = { 1.0 }
206+
) { _, state ->
207+
state.copy(hasPickaxe = true)
208+
}
209+
210+
// Build shelter (requires axe and pickaxe)
211+
action(
212+
name = "Build shelter",
213+
precondition = { state ->
214+
state.hasAxe && state.hasPickaxe && !state.hasShelter
215+
},
216+
belief = { state -> state.copy(hasShelter = true) },
217+
cost = { 3.0 }
218+
) { _, state ->
219+
state.copy(hasShelter = true)
220+
}
221+
222+
goal(
223+
name = "Build shelter",
224+
condition = { state -> state.hasShelter }
225+
)
226+
}
227+
228+
val strategy = AIAgentPlannerStrategy("goap-complex-test", planner)
229+
val mockExecutor = getMockExecutor {
230+
mockLLMAnswer("OK").asDefaultResponse
231+
}
232+
233+
val agentConfig = AIAgentConfig(
234+
prompt = prompt("goap") { system("GOAP agent") },
235+
model = OllamaModels.Meta.LLAMA_3_2,
236+
maxAgentIterations = 50
237+
)
238+
239+
val agent = PlannerAIAgent(
240+
promptExecutor = mockExecutor,
241+
strategy = strategy,
242+
agentConfig = agentConfig
243+
)
244+
245+
val initialState = ComplexState()
246+
val finalState = agent.run(initialState)
247+
248+
assertTrue(finalState.hasWood, "Agent should have gathered wood")
249+
assertTrue(finalState.hasStone, "Agent should have gathered stone")
250+
assertTrue(finalState.hasAxe, "Agent should have crafted an axe")
251+
assertTrue(finalState.hasPickaxe, "Agent should have crafted a pickaxe")
252+
assertTrue(finalState.hasShelter, "Agent should have built the shelter")
253+
}
254+
}

0 commit comments

Comments
 (0)