Skip to content

Commit ae773c5

Browse files
committed
Update planner_agent.py
1 parent 2412026 commit ae773c5

File tree

1 file changed

+115
-33
lines changed

1 file changed

+115
-33
lines changed

src/backend/kernel_agents/planner_agent.py

Lines changed: 115 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -277,24 +277,74 @@ async def _create_structured_plan(self, input_task: InputTask) -> Tuple[Plan, Li
277277
raise RuntimeError("Failed to initialize Azure AI Agent for planning")
278278

279279
# Get response from the Azure AI Agent
280-
response = await self._azure_ai_agent.invoke_async(instruction)
281-
response_content = str(response).strip()
280+
# Based on the method signature, invoke takes only named arguments, not positional ones
281+
logging.info(f"Invoking PlannerAgent with instruction length: {len(instruction)}")
282+
283+
# Create kernel arguments
284+
kernel_args = KernelArguments()
285+
kernel_args["input"] = instruction
286+
287+
# Call invoke with proper keyword arguments
288+
response_content = ""
289+
290+
# Use keyword arguments instead of positional arguments
291+
# Based on the method signature, we need to pass 'arguments' and possibly 'kernel'
292+
async_generator = self._azure_ai_agent.invoke(arguments=kernel_args)
293+
294+
# Collect the response from the async generator
295+
async for chunk in async_generator:
296+
if chunk is not None:
297+
response_content += str(chunk)
298+
299+
# Debug the response
300+
logging.info(f"Response content length: {len(response_content)}")
301+
logging.debug(f"Response content first 500 chars: {response_content[:500]}")
302+
303+
# Check if response is empty or whitespace
304+
if not response_content or response_content.isspace():
305+
raise ValueError("Received empty response from Azure AI Agent")
282306

283307
# Parse the JSON response using the structured output model
284308
try:
285309
# First try to parse using Pydantic model
286310
try:
287311
parsed_result = StructuredOutputPlan.parse_raw(response_content)
288-
except Exception:
312+
except Exception as e1:
313+
logging.warning(f"Failed to parse direct JSON with Pydantic: {str(e1)}")
314+
289315
# If direct parsing fails, try to extract JSON first
290316
json_match = re.search(r'```json\s*(.*?)\s*```', response_content, re.DOTALL)
291317
if json_match:
292318
json_content = json_match.group(1)
293-
parsed_result = StructuredOutputPlan.parse_raw(json_content)
319+
logging.info(f"Found JSON content in markdown code block, length: {len(json_content)}")
320+
try:
321+
parsed_result = StructuredOutputPlan.parse_raw(json_content)
322+
except Exception as e2:
323+
logging.warning(f"Failed to parse extracted JSON with Pydantic: {str(e2)}")
324+
# Try conventional JSON parsing as fallback
325+
json_data = json.loads(json_content)
326+
parsed_result = StructuredOutputPlan.parse_obj(json_data)
294327
else:
295-
# Try parsing as regular JSON then convert to Pydantic model
296-
json_data = json.loads(response_content)
297-
parsed_result = StructuredOutputPlan.parse_obj(json_data)
328+
# Try to extract JSON without code blocks - maybe it's embedded in text
329+
# Look for patterns like { ... } that contain "initial_goal" and "steps"
330+
json_pattern = r'\{.*?"initial_goal".*?"steps".*?\}'
331+
alt_match = re.search(json_pattern, response_content, re.DOTALL)
332+
333+
if alt_match:
334+
potential_json = alt_match.group(0)
335+
logging.info(f"Found potential JSON in text, length: {len(potential_json)}")
336+
try:
337+
json_data = json.loads(potential_json)
338+
parsed_result = StructuredOutputPlan.parse_obj(json_data)
339+
except Exception as e3:
340+
logging.warning(f"Failed to parse potential JSON: {str(e3)}")
341+
# If all extraction attempts fail, try parsing the whole response as JSON
342+
json_data = json.loads(response_content)
343+
parsed_result = StructuredOutputPlan.parse_obj(json_data)
344+
else:
345+
# If we can't find JSON patterns, create a fallback plan from the text
346+
logging.info("Using fallback plan creation from text response")
347+
return await self._create_fallback_plan_from_text(input_task, response_content)
298348

299349
# Extract plan details
300350
initial_goal = parsed_result.initial_goal
@@ -372,9 +422,11 @@ async def _create_structured_plan(self, input_task: InputTask) -> Tuple[Plan, Li
372422
return plan, steps
373423

374424
except Exception as e:
375-
# If JSON parsing fails, use regex to extract steps
376-
logging.warning(f"Failed to parse JSON response: {e}. Falling back to text parsing.")
377-
return await self._create_plan_from_text(input_task, response_content)
425+
# If JSON parsing fails, log error and create error plan
426+
logging.exception(f"Failed to parse JSON response: {e}")
427+
logging.info(f"Raw response was: {response_content[:1000]}...")
428+
# Try a fallback approach
429+
return await self._create_fallback_plan_from_text(input_task, response_content)
378430

379431
except Exception as e:
380432
logging.exception(f"Error creating structured plan: {e}")
@@ -403,8 +455,8 @@ async def _create_structured_plan(self, input_task: InputTask) -> Tuple[Plan, Li
403455

404456
await self._memory_store.add_plan(error_plan)
405457
return error_plan, []
406-
407-
async def _create_plan_from_text(self, input_task: InputTask, text_content: str) -> Tuple[Plan, List[Step]]:
458+
459+
async def _create_fallback_plan_from_text(self, input_task: InputTask, text_content: str) -> Tuple[Plan, List[Step]]:
408460
"""Create a plan from unstructured text when JSON parsing fails.
409461
410462
Args:
@@ -414,6 +466,8 @@ async def _create_plan_from_text(self, input_task: InputTask, text_content: str)
414466
Returns:
415467
Tuple containing the created plan and list of steps
416468
"""
469+
logging.info("Creating fallback plan from text content")
470+
417471
# Extract goal from the text (first line or use input task description)
418472
goal_match = re.search(r"(?:Goal|Initial Goal|Plan):\s*(.+?)(?:\n|$)", text_content)
419473
goal = goal_match.group(1).strip() if goal_match else input_task.description
@@ -424,7 +478,8 @@ async def _create_plan_from_text(self, input_task: InputTask, text_content: str)
424478
session_id=input_task.session_id,
425479
user_id=self._user_id,
426480
initial_goal=goal,
427-
overall_status=PlanStatus.in_progress
481+
overall_status=PlanStatus.in_progress,
482+
summary=f"Plan created from {input_task.description}"
428483
)
429484

430485
# Store the plan
@@ -439,32 +494,57 @@ async def _create_plan_from_text(self, input_task: InputTask, text_content: str)
439494
step_pattern = re.compile(r'(\d+)[.:\)]\s*([^:]*?):\s*(.*?)(?=\d+[.:\)]|$)', re.DOTALL)
440495
matches = step_pattern.findall(text_content)
441496

497+
# If still no matches, look for bullet points or numbered lists
498+
if not matches:
499+
step_pattern = re.compile(r'[•\-*]\s*([^:]*?):\s*(.*?)(?=[•\-*]|$)', re.DOTALL)
500+
bullet_matches = step_pattern.findall(text_content)
501+
if bullet_matches:
502+
# Convert bullet matches to our expected format (number, agent, action)
503+
matches = []
504+
for i, (agent_text, action) in enumerate(bullet_matches, 1):
505+
matches.append((str(i), agent_text.strip(), action.strip()))
506+
442507
steps = []
443-
for match in matches:
444-
number = match[0].strip()
445-
agent_text = match[1].strip()
446-
action = match[2].strip()
447-
448-
# Clean up agent name
449-
agent = re.sub(r'\s+', '', agent_text)
450-
if not agent or agent not in self._available_agents:
451-
agent = "GenericAgent" # Default to GenericAgent if not recognized
452-
453-
# Create and store the step
454-
step = Step(
508+
# If we found no steps at all, create at least one generic step
509+
if not matches:
510+
generic_step = Step(
455511
id=str(uuid.uuid4()),
456512
plan_id=plan.id,
457513
session_id=input_task.session_id,
458514
user_id=self._user_id,
459-
action=action,
460-
agent=agent,
515+
action=f"Process the request: {input_task.description}",
516+
agent="GenericAgent",
461517
status=StepStatus.planned,
462518
human_approval_status=HumanFeedbackStatus.requested
463519
)
464-
465-
await self._memory_store.add_step(step)
466-
steps.append(step)
467-
520+
await self._memory_store.add_step(generic_step)
521+
steps.append(generic_step)
522+
else:
523+
for match in matches:
524+
number = match[0].strip()
525+
agent_text = match[1].strip()
526+
action = match[2].strip()
527+
528+
# Clean up agent name
529+
agent = re.sub(r'\s+', '', agent_text)
530+
if not agent or agent not in self._available_agents:
531+
agent = "GenericAgent" # Default to GenericAgent if not recognized
532+
533+
# Create and store the step
534+
step = Step(
535+
id=str(uuid.uuid4()),
536+
plan_id=plan.id,
537+
session_id=input_task.session_id,
538+
user_id=self._user_id,
539+
action=action,
540+
agent=agent,
541+
status=StepStatus.planned,
542+
human_approval_status=HumanFeedbackStatus.requested
543+
)
544+
545+
await self._memory_store.add_step(step)
546+
steps.append(step)
547+
468548
return plan, steps
469549

470550
def _generate_instruction(self, objective: str) -> str:
@@ -482,7 +562,9 @@ def _generate_instruction(self, objective: str) -> str:
482562
# Create list of available tools
483563
tools_str = "\n".join(self._agent_tools_list) if self._agent_tools_list else "Various specialized tools"
484564

485-
# Use double curly braces to escape them in f-strings
565+
# Build the instruction, avoiding backslashes in f-string expressions
566+
objective_part = f"Your objective is:\n{objective}" if objective else "When given an objective, analyze it and create a plan to accomplish it."
567+
486568
return f"""
487569
You are the Planner, an AI orchestrator that manages a group of AI agents to accomplish tasks.
488570
@@ -492,7 +574,7 @@ def _generate_instruction(self, objective: str) -> str:
492574
493575
These actions are passed to the specific agent. Make sure the action contains all the information required for the agent to execute the task.
494576
495-
{f"Your objective is:\\n{objective}" if objective else "When given an objective, analyze it and create a plan to accomplish it."}
577+
{objective_part}
496578
497579
The agents you have access to are:
498580
{agents_str}

0 commit comments

Comments
 (0)