Skip to content

Commit 39d41a7

Browse files
moonbox3glorious-beard
authored andcommitted
Python: Improve plan and execute sample (microsoft#11370)
### Motivation and Context The current version of the processes' "plan and execute" sample worked but it didn't format the output or intermediate output in a nice way. This PR improves the formatting of the output and thus the final answer. <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> ### Description Improve the sample. <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄
1 parent 8055f4b commit 39d41a7

File tree

1 file changed

+78
-57
lines changed

1 file changed

+78
-57
lines changed

python/samples/concepts/processes/plan_and_execute.py

+78-57
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"""
2626
The following code demonstrates a simple plan and execute process using the Semantic Kernel Process Framework.
2727
It defines a multi-step workflow that leverages OpenAI's Responses API and integrated web search tools to generate,
28-
refine, and execute a plan based on a user's query.
28+
refine, and execute a plan based on a user's query. An OpenAI api key is required to run this sample. The Azure OpenAI
29+
Responses API does not yet support the web search tool.
2930
3031
The process is composed of several steps:
3132
@@ -45,7 +46,9 @@
4546
"""
4647

4748

48-
# Define a method to get a response from the OpenAI agent
49+
#
50+
# 1) Helper to run OpenAI agent
51+
#
4952
async def run_openai_agent(instructions: str, prompt: str, agent_name: str = "GenericAgent") -> str:
5053
client, model = OpenAIResponsesAgent.setup_resources()
5154
agent_tools = [OpenAIResponsesAgent.configure_web_search_tool()]
@@ -62,7 +65,7 @@ async def run_openai_agent(instructions: str, prompt: str, agent_name: str = "Ge
6265

6366

6467
#
65-
# 1) Global Events
68+
# 2) Global Events
6669
#
6770
class PlanExecuteEvents(str, Enum):
6871
StartProcess = "StartProcess"
@@ -75,7 +78,7 @@ class PlanExecuteEvents(str, Enum):
7578

7679

7780
#
78-
# 2) Planner Step
81+
# 3) Planner Step
7982
#
8083
class PlannerStepState:
8184
times_called: int = 0
@@ -111,7 +114,7 @@ async def create_plan(self, user_request: str, context: KernelProcessStepContext
111114

112115

113116
#
114-
# 3) Replan Step
117+
# 4) Replan Step
115118
#
116119
class ReplanStepState:
117120
times_called: int = 0
@@ -147,7 +150,7 @@ async def refine_plan(self, payload: dict, context: KernelProcessStepContext) ->
147150

148151

149152
#
150-
# 4) Execute Step
153+
# 5) Execute Step
151154
#
152155
class ExecuteStepState:
153156
current_index: int = 0
@@ -163,19 +166,16 @@ async def activate(self, state: KernelProcessStepState[ExecuteStepState]):
163166
@kernel_function(name=EXECUTE_PLAN)
164167
async def execute_plan(self, payload: dict, context: KernelProcessStepContext) -> dict:
165168
plan = payload["plan"]
166-
partials = payload.get("partials", []) # May be empty on first run
169+
partials = payload.get("partials", [])
167170

168171
if self.state.current_index >= len(plan):
169-
# No more tasks
170172
return {
171173
"partial_result": "All tasks done",
172174
"plan": plan,
173175
"current_index": self.state.current_index,
174176
}
175177

176178
current_task = plan[self.state.current_index]
177-
178-
# Incorporate partial context into the prompt:
179179
prompt = (
180180
f"So far we have these partial results:\n\n{chr(10).join(partials)}\n\nNow your task is: {current_task}"
181181
)
@@ -185,7 +185,6 @@ async def execute_plan(self, payload: dict, context: KernelProcessStepContext) -
185185
prompt=prompt,
186186
agent_name="ExecuteAgent",
187187
)
188-
189188
partial_result = response.strip()
190189

191190
executed_index = self.state.current_index
@@ -199,7 +198,7 @@ async def execute_plan(self, payload: dict, context: KernelProcessStepContext) -
199198

200199

201200
#
202-
# 5) Decision Step
201+
# 6) Decision Step
203202
#
204203
class DecisionStepState(KernelBaseModel):
205204
partials: list[str] = Field(default_factory=list)
@@ -223,38 +222,41 @@ async def make_decision(self, data: dict, context: KernelProcessStepContext):
223222
if partial_result and partial_result.lower() != "all tasks done":
224223
self.state.partials.append(partial_result)
225224

226-
# (A) If "All tasks done"
225+
# A) If "All tasks done"
227226
if partial_result.strip().lower().startswith("all tasks done"):
228-
final_text = "[FINAL] All tasks completed.\n\n=== Aggregated Partial Results ===\n" + "\n\n".join(
229-
self.state.partials
230-
)
231-
await context.emit_event(PlanExecuteEvents.PlanFinished, data=final_text)
227+
# No more tasks
228+
final_text = "\n".join(self.state.partials)
229+
payload = {
230+
"type": "final",
231+
"content": final_text.strip(),
232+
}
233+
await context.emit_event(PlanExecuteEvents.PlanFinished, data=payload)
232234
return
233235

234-
# (B) If physically done all tasks
236+
# B) If physically done all tasks
235237
if current_index >= len(plan):
236-
final_text = "[FINAL] No more tasks.\n\n=== Aggregated Partial Results ===\n" + "\n\n".join(
237-
self.state.partials
238-
)
239-
await context.emit_event(PlanExecuteEvents.PlanFinished, data=final_text)
238+
final_text = "\n".join(self.state.partials)
239+
payload = {
240+
"type": "final",
241+
"content": final_text.strip(),
242+
}
243+
await context.emit_event(PlanExecuteEvents.PlanFinished, data=payload)
240244
return
241245

242-
# (C) Let LLM decide: continue or replan
246+
# C) Otherwise: let LLM decide whether to continue or replan
243247
prompt = (
244248
"We have a plan with remaining tasks.\n"
245249
"PARTIAL RESULTS SO FAR:\n" + "\n".join(self.state.partials) + "\n\n"
246250
"Decide: 'continue' with the next step or 'replan' if something is invalid. "
247251
"DO NOT say 'finish' if tasks remain. Return only 'continue' or 'replan'."
248252
)
249-
250253
response = await run_openai_agent(
251254
instructions="Plan orchestrator with web-search if needed. Only respond 'continue' or 'replan'.",
252255
prompt=prompt,
253256
agent_name="DecisionAgent",
254257
)
255258

256259
decision = response.strip().lower()
257-
258260
print(f"[DecisionStep] LLM decision: {decision}")
259261
self.state.last_decision = decision
260262

@@ -276,10 +278,11 @@ async def make_decision(self, data: dict, context: KernelProcessStepContext):
276278

277279

278280
#
279-
# 6) Output Step
281+
# 7) Output Step
280282
#
281-
class OutputStepState:
282-
last_message: str = ""
283+
class OutputStepState(KernelBaseModel):
284+
debug_history: list[str] = Field(default_factory=list)
285+
final_answer: str = ""
283286

284287

285288
class OutputStep(KernelProcessStep[OutputStepState]):
@@ -290,13 +293,22 @@ async def activate(self, state: KernelProcessStepState[OutputStepState]):
290293
self.state = state.state
291294

292295
@kernel_function(name=SHOW_MESSAGE)
293-
async def show_message(self, message: str | list[str]):
294-
self.state.last_message = message
295-
print(f"[OutputStep] {message}")
296+
async def show_message(self, message: dict):
297+
"""Handles either debug messages or final messages."""
298+
msg_type = message.get("type", "debug")
299+
content = message.get("content", "")
300+
301+
if msg_type == "debug":
302+
self.state.debug_history.append(content)
303+
print(content)
304+
else:
305+
# final
306+
self.state.final_answer = content
307+
print("[OutputStep] Storing final result:", content)
296308

297309

298310
#
299-
# 7) Build the Process
311+
# 8) Build the Process
300312
#
301313
def build_process() -> KernelProcess:
302314
builder = ProcessBuilder(name="GeneralPlanAndExecute")
@@ -311,7 +323,7 @@ def build_process() -> KernelProcess:
311323
# 1) Start => Planner
312324
builder.on_input_event(PlanExecuteEvents.StartProcess).send_event_to(target=planner, parameter_name="user_request")
313325

314-
# 2) Planner => Executor + Output
326+
# 2) Planner => Executor + Output (debug)
315327
planner.on_function_result(PlannerStep.CREATE_PLAN).send_event_to(target=executor, parameter_name="payload")
316328

317329
planner.on_function_result(PlannerStep.CREATE_PLAN).send_event_to(
@@ -339,7 +351,7 @@ async def main():
339351

340352
# Provide any user question
341353
user_question = "Where was the quarterback of the winning team in the 2014 Super Bowl born?"
342-
print(f"Starting process with: {user_question}")
354+
print(f"Starting process with: '{user_question}'")
343355

344356
process = build_process()
345357
async with await start_local_process(
@@ -351,38 +363,47 @@ async def main():
351363
visibility=KernelProcessEventVisibility.Public,
352364
),
353365
) as process_context:
354-
# Finally, get the Output
366+
# Retrieve final state
355367
process_state = await process_context.get_state()
356368
output_step_state: KernelProcessStepState[OutputStepState] = next(
357369
(s.state for s in process_state.steps if s.state.name == "OutputStep"), None
358370
)
359-
final_msg = output_step_state.state.last_message if output_step_state else "No final message."
360-
print(f"\n\n[Final State] => {final_msg}")
371+
372+
if output_step_state:
373+
# Final user-facing answer:
374+
final_answer = output_step_state.state.final_answer.strip()
375+
376+
print("\n[Final State]:")
377+
print(final_answer)
378+
else:
379+
print("[Final State]: No final message.")
361380

362381
"""
363-
Sample Output:
382+
Starting process with: 'Where was the quarterback of the winning team in the 2014 Super Bowl born?'
383+
[PlannerStep] Created plan: ['Identify the Winning Team:** Find out which team won the 2014 Super Bowl.
384+
[NFL.com](https://www.nfl.com/)', 'Locate the Quarterback:** Determine who was the quarterback for the winning
385+
team during that game.', 'Research Birthplace:** Search for the birthplace of the identified quarterback.
386+
[Wikipedia](https://www.wikipedia.org/)'] (times_called=1)
364387
365-
Starting process with: Where was the quarterback of the winning team in the 2014 Super Bowl born?
366-
[PlannerStep] Created plan: ['1. Identify the winning team and quarterback of the 2014 Super Bowl.', '2. Find the
367-
birthplace of that quarterback.'] (times_called=1)
368-
[OutputStep] {'plan': ['1. Identify the winning team and quarterback of the 2014 Super Bowl.', '2. Find the
369-
birthplace of that quarterback.']}
370388
[DecisionStep] LLM decision: continue
371389
[DecisionStep] LLM decision: continue
372-
[OutputStep] [FINAL] All tasks completed.
373-
374-
=== Aggregated Partial Results ===
375-
The winning team of the 2014 Super Bowl (Super Bowl XLVIII) was the Seattle Seahawks. The quarterback for the
376-
Seahawks at that time was Russell Wilson.
377-
378-
Russell Wilson was born in Cincinnati, Ohio.
379-
380-
381-
[Final State] => [FINAL] All tasks completed.
382-
383-
=== Aggregated Partial Results ===
384-
The winning team of the 2014 Super Bowl (Super Bowl XLVIII) was the Seattle Seahawks. The quarterback for the
385-
Seahawks at that time was Russell Wilson.
390+
[DecisionStep] LLM decision: continue
391+
[OutputStep] Storing final result: The Seattle Seahawks won Super Bowl XLVIII in 2014, defeating the Denver Broncos
392+
43-8. The Seahawks' defense dominated the game, forcing four turnovers and limiting the Broncos' high-scoring
393+
offense. Linebacker Malcolm Smith was named Super Bowl MVP after returning an interception 69 yards for a
394+
touchdown. ([nfl.com](https://www.nfl.com/news/seattle-seahawks-d-dominates-manning-denver-broncos-to-win-supe-0ap2000000323056?utm_source=openai))
395+
The quarterback for the Seattle Seahawks during Super Bowl XLVIII was Russell Wilson.
396+
Russell Wilson, the quarterback for the Seattle Seahawks during Super Bowl XLVIII, was born on November 29, 1988,
397+
in Cincinnati, Ohio. ([britannica.com](https://www.britannica.com/facts/Russell-Wilson?utm_source=openai))
398+
399+
[Final State]:
400+
The Seattle Seahawks won Super Bowl XLVIII in 2014, defeating the Denver Broncos 43-8. The Seahawks' defense
401+
dominated the game, forcing four turnovers and limiting the Broncos' high-scoring offense. Linebacker Malcolm
402+
Smith was named Super Bowl MVP after returning an interception 69 yards for a touchdown.
403+
([nfl.com](https://www.nfl.com/news/seattle-seahawks-d-dominates-manning-denver-broncos-to-win-supe-0ap2000000323056?utm_source=openai))
404+
The quarterback for the Seattle Seahawks during Super Bowl XLVIII was Russell Wilson.
405+
Russell Wilson, the quarterback for the Seattle Seahawks during Super Bowl XLVIII, was born on November 29, 1988,
406+
in Cincinnati, Ohio. ([britannica.com](https://www.britannica.com/facts/Russell-Wilson?utm_source=openai))
386407
"""
387408

388409

0 commit comments

Comments
 (0)