Skip to content

Commit c27de44

Browse files
committed
Merge branch 'feature/azd-semantickernel' of https://github.com/microsoft/Multi-Agent-Custom-Automation-Engine-Solution-Accelerator into azd-semantickernel-marktayl
2 parents 9810754 + 2c63e64 commit c27de44

File tree

1 file changed

+91
-227
lines changed

1 file changed

+91
-227
lines changed

src/backend/kernel_agents/planner_agent.py

Lines changed: 91 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,16 @@ async def _create_structured_plan(self, input_task: InputTask) -> Tuple[Plan, Li
263263
"""
264264
try:
265265
# Generate the instruction for the LLM
266+
logging.info("Generating instruction for the LLM")
267+
logging.debug(f"Input: {input_task}")
268+
logging.debug(f"Available agents: {self._available_agents}")
266269

267270
instruction = self._generate_instruction(input_task.description)
268271

272+
logging.info(f"Generated instruction: {instruction}")
273+
# Log the input task for debugging
274+
logging.info(f"Creating plan for task: '{input_task.description}'")
275+
logging.info(f"Using available agents: {self._available_agents}")
269276

270277
# Use the Azure AI Agent instead of direct function invocation
271278
if self._azure_ai_agent is None:
@@ -293,6 +300,7 @@ async def _create_structured_plan(self, input_task: InputTask) -> Tuple[Plan, Li
293300
arguments=kernel_args,
294301
settings={
295302
"temperature": 0.0, # Keep temperature low for consistent planning
303+
"max_tokens": 4096 # Ensure we have enough tokens for the full plan
296304
}
297305
)
298306

@@ -301,267 +309,123 @@ async def _create_structured_plan(self, input_task: InputTask) -> Tuple[Plan, Li
301309
if chunk is not None:
302310
response_content += str(chunk)
303311

304-
305312
logging.info(f"Response content: {response_content}")
306313

307314
# Check if response is empty or whitespace
308315
if not response_content or response_content.isspace():
309316
raise ValueError("Received empty response from Azure AI Agent")
310317

311-
# Parse the JSON response using the structured output model
318+
# Parse the JSON response directly to PlannerResponsePlan
319+
parsed_result = None
320+
321+
# Try to parse the raw response first
312322
try:
313-
# First try to parse using Pydantic model
314-
try:
315-
parsed_result = PlannerResponsePlan.parse_raw(response_content)
316-
except Exception as e1:
317-
logging.warning(f"Failed to parse direct JSON with Pydantic: {str(e1)}")
318-
319-
# If direct parsing fails, try to extract JSON first
320-
json_match = re.search(r'```json\s*(.*?)\s*```', response_content, re.DOTALL)
321-
if json_match:
322-
json_content = json_match.group(1)
323-
logging.info(f"Found JSON content in markdown code block, length: {len(json_content)}")
324-
try:
325-
parsed_result = PlannerResponsePlan.parse_raw(json_content)
326-
except Exception as e2:
327-
logging.warning(f"Failed to parse extracted JSON with Pydantic: {str(e2)}")
328-
# Try conventional JSON parsing as fallback
329-
json_data = json.loads(json_content)
330-
parsed_result = PlannerResponsePlan.parse_obj(json_data)
331-
else:
332-
# Try to extract JSON without code blocks - maybe it's embedded in text
333-
# Look for patterns like { ... } that contain "initial_goal" and "steps"
334-
json_pattern = r'\{.*?"initial_goal".*?"steps".*?\}'
335-
alt_match = re.search(json_pattern, response_content, re.DOTALL)
336-
337-
if alt_match:
338-
potential_json = alt_match.group(0)
339-
logging.info(f"Found potential JSON in text, length: {len(potential_json)}")
340-
try:
341-
json_data = json.loads(potential_json)
342-
parsed_result = PlannerResponsePlan.parse_obj(json_data)
343-
except Exception as e3:
344-
logging.warning(f"Failed to parse potential JSON: {str(e3)}")
345-
# If all extraction attempts fail, try parsing the whole response as JSON
346-
json_data = json.loads(response_content)
347-
parsed_result = PlannerResponsePlan.parse_obj(json_data)
348-
else:
349-
# If we can't find JSON patterns, create a fallback plan from the text
350-
logging.info("Using fallback plan creation from text response")
351-
return await self._create_fallback_plan_from_text(input_task, response_content)
323+
parsed_result = PlannerResponsePlan.parse_raw(response_content)
324+
except Exception as e:
325+
logging.warning(f"Failed to parse raw response: {e}")
352326

353-
# Extract plan details and log for debugging
354-
initial_goal = parsed_result.initial_goal
355-
steps_data = parsed_result.steps
356-
summary = parsed_result.summary_plan_and_steps
357-
human_clarification_request = parsed_result.human_clarification_request
327+
# Try to extract JSON from markdown code blocks
328+
json_match = re.search(r'```(?:json)?\s*(.*?)\s*```', response_content, re.DOTALL)
329+
if json_match:
330+
json_content = json_match.group(1)
331+
logging.info(f"Found JSON in code block, attempting to parse")
332+
parsed_result = PlannerResponsePlan.parse_raw(json_content)
333+
else:
334+
# If still not parsed, raise the error to be handled by outer exception
335+
raise ValueError(f"Failed to parse response as PlannerResponsePlan: {e}")
336+
337+
# At this point, we have a valid parsed_result or an exception was raised
338+
339+
# Extract plan details
340+
initial_goal = parsed_result.initial_goal
341+
steps_data = parsed_result.steps
342+
summary = parsed_result.summary_plan_and_steps
343+
human_clarification_request = parsed_result.human_clarification_request
344+
345+
# Create the Plan instance
346+
plan = Plan(
347+
id=str(uuid.uuid4()),
348+
session_id=input_task.session_id,
349+
user_id=self._user_id,
350+
initial_goal=initial_goal,
351+
overall_status=PlanStatus.in_progress,
352+
summary=summary,
353+
human_clarification_request=human_clarification_request
354+
)
355+
356+
# Store the plan
357+
await self._memory_store.add_plan(plan)
358+
359+
track_event_if_configured(
360+
"Planner - Initial plan and added into the cosmos",
361+
{
362+
"session_id": input_task.session_id,
363+
"user_id": self._user_id,
364+
"initial_goal": initial_goal,
365+
"overall_status": PlanStatus.in_progress,
366+
"source": "PlannerAgent",
367+
"summary": summary,
368+
"human_clarification_request": human_clarification_request,
369+
},
370+
)
371+
372+
# Create steps from the parsed data
373+
steps = []
374+
for step_data in steps_data:
375+
action = step_data.action
376+
agent_name = step_data.agent
358377

359-
# Log potential mismatches between task and plan for debugging
360-
if "onboard" in input_task.description.lower() and "marketing" in initial_goal.lower():
361-
logging.warning(f"Potential mismatch: Task was about onboarding but plan goal mentions marketing: {initial_goal}")
362-
363-
# Log the steps and agent assignments for debugging
364-
for i, step in enumerate(steps_data):
365-
logging.info(f"Step {i+1} - Agent: {step.agent}, Action: {step.action}")
378+
# Validate agent name
379+
if agent_name not in self._available_agents:
380+
logging.warning(f"Invalid agent name: {agent_name}, defaulting to GenericAgent")
381+
agent_name = "GenericAgent"
366382

367-
# Create the Plan instance
368-
plan = Plan(
383+
# Create the step
384+
step = Step(
369385
id=str(uuid.uuid4()),
386+
plan_id=plan.id,
370387
session_id=input_task.session_id,
371388
user_id=self._user_id,
372-
initial_goal=initial_goal,
373-
overall_status=PlanStatus.in_progress,
374-
summary=summary,
375-
human_clarification_request=human_clarification_request
389+
action=action,
390+
agent=agent_name,
391+
status=StepStatus.planned,
392+
human_approval_status=HumanFeedbackStatus.requested
376393
)
377394

378-
# Store the plan
379-
await self._memory_store.add_plan(plan)
395+
# Store the step
396+
await self._memory_store.add_step(step)
397+
steps.append(step)
380398

381399
track_event_if_configured(
382-
"Planner - Initial plan and added into the cosmos",
400+
"Planner - Added planned individual step into the cosmos",
383401
{
402+
"plan_id": plan.id,
403+
"action": action,
404+
"agent": agent_name,
405+
"status": StepStatus.planned,
384406
"session_id": input_task.session_id,
385407
"user_id": self._user_id,
386-
"initial_goal": initial_goal,
387-
"overall_status": PlanStatus.in_progress,
388-
"source": "PlannerAgent",
389-
"summary": summary,
390-
"human_clarification_request": human_clarification_request,
408+
"human_approval_status": HumanFeedbackStatus.requested,
391409
},
392410
)
393-
394-
# Create steps from the parsed data
395-
steps = []
396-
for step_data in steps_data:
397-
action = step_data.action
398-
agent_name = step_data.agent
399-
400-
# Log any unusual agent assignments for debugging
401-
if "onboard" in input_task.description.lower() and agent_name != "HrAgent":
402-
logging.warning(f"UNUSUAL AGENT ASSIGNMENT: Task contains 'onboard' but assigned to {agent_name} instead of HrAgent")
403-
404-
# Validate agent name
405-
if agent_name not in self._available_agents:
406-
logging.warning(f"Invalid agent name: {agent_name}, defaulting to GenericAgent")
407-
agent_name = "GenericAgent"
408-
409-
# Create the step
410-
step = Step(
411-
id=str(uuid.uuid4()),
412-
plan_id=plan.id,
413-
session_id=input_task.session_id,
414-
user_id=self._user_id,
415-
action=action,
416-
agent=agent_name,
417-
status=StepStatus.planned,
418-
human_approval_status=HumanFeedbackStatus.requested
419-
)
420-
421-
# Store the step
422-
await self._memory_store.add_step(step)
423-
steps.append(step)
424-
425-
track_event_if_configured(
426-
"Planner - Added planned individual step into the cosmos",
427-
{
428-
"plan_id": plan.id,
429-
"action": action,
430-
"agent": agent_name,
431-
"status": StepStatus.planned,
432-
"session_id": input_task.session_id,
433-
"user_id": self._user_id,
434-
"human_approval_status": HumanFeedbackStatus.requested,
435-
},
436-
)
437-
438-
return plan, steps
439-
440-
except Exception as e:
441-
# If JSON parsing fails, log error and create error plan
442-
logging.exception(f"Failed to parse JSON response: {e}")
443-
logging.info(f"Raw response was: {response_content[:1000]}...")
444-
# Try a fallback approach
445-
return await self._create_fallback_plan_from_text(input_task, response_content)
411+
412+
return plan, steps
446413

447414
except Exception as e:
448415
logging.exception(f"Error creating structured plan: {e}")
449416

450417
track_event_if_configured(
451-
f"Planner - Error in create_structured_plan: {e} into the cosmos",
418+
f"Planner - Error in create_structured_plan: {e}",
452419
{
453420
"session_id": input_task.session_id,
454421
"user_id": self._user_id,
455-
"initial_goal": "Error generating plan",
456-
"overall_status": PlanStatus.failed,
422+
"error": str(e),
457423
"source": "PlannerAgent",
458-
"summary": f"Error generating plan: {e}",
459424
},
460425
)
461426

462-
# Create an error plan
463-
error_plan = Plan(
464-
id=str(uuid.uuid4()),
465-
session_id=input_task.session_id,
466-
user_id=self._user_id,
467-
initial_goal="Error generating plan",
468-
overall_status=PlanStatus.failed,
469-
summary=f"Error generating plan: {str(e)}"
470-
)
471-
472-
await self._memory_store.add_plan(error_plan)
473-
return error_plan, []
474-
475-
async def _create_fallback_plan_from_text(self, input_task: InputTask, text_content: str) -> Tuple[Plan, List[Step]]:
476-
"""Create a plan from unstructured text when JSON parsing fails.
477-
478-
Args:
479-
input_task: The input task
480-
text_content: The text content from the LLM
481-
482-
Returns:
483-
Tuple containing the created plan and list of steps
484-
"""
485-
logging.info("Creating fallback plan from text content")
486-
487-
# Extract goal from the text (first line or use input task description)
488-
goal_match = re.search(r"(?:Goal|Initial Goal|Plan):\s*(.+?)(?:\n|$)", text_content)
489-
goal = goal_match.group(1).strip() if goal_match else input_task.description
490-
491-
# Create the plan
492-
plan = Plan(
493-
id=str(uuid.uuid4()),
494-
session_id=input_task.session_id,
495-
user_id=self._user_id,
496-
initial_goal=goal,
497-
overall_status=PlanStatus.in_progress,
498-
summary=f"Plan created from {input_task.description}"
499-
)
500-
501-
# Store the plan
502-
await self._memory_store.add_plan(plan)
503-
504-
# Parse steps using regex
505-
step_pattern = re.compile(r'(?:Step|)\s*(\d+)[:.]\s*\*?\*?(?:Agent|):\s*\*?([^:*\n]+)\*?[:\s]*(.+?)(?=(?:Step|)\s*\d+[:.]\s*|$)', re.DOTALL)
506-
matches = step_pattern.findall(text_content)
507-
508-
if not matches:
509-
# Fallback to simpler pattern
510-
step_pattern = re.compile(r'(\d+)[.:\)]\s*([^:]*?):\s*(.*?)(?=\d+[.:\)]|$)', re.DOTALL)
511-
matches = step_pattern.findall(text_content)
512-
513-
# If still no matches, look for bullet points or numbered lists
514-
if not matches:
515-
step_pattern = re.compile(r'[•\-*]\s*([^:]*?):\s*(.*?)(?=[•\-*]|$)', re.DOTALL)
516-
bullet_matches = step_pattern.findall(text_content)
517-
if bullet_matches:
518-
# Convert bullet matches to our expected format (number, agent, action)
519-
matches = []
520-
for i, (agent_text, action) in enumerate(bullet_matches, 1):
521-
matches.append((str(i), agent_text.strip(), action.strip()))
522-
523-
steps = []
524-
# If we found no steps at all, create at least one generic step
525-
if not matches:
526-
generic_step = Step(
527-
id=str(uuid.uuid4()),
528-
plan_id=plan.id,
529-
session_id=input_task.session_id,
530-
user_id=self._user_id,
531-
action=f"Process the request: {input_task.description}",
532-
agent="GenericAgent",
533-
status=StepStatus.planned,
534-
human_approval_status=HumanFeedbackStatus.requested
535-
)
536-
await self._memory_store.add_step(generic_step)
537-
steps.append(generic_step)
538-
else:
539-
for match in matches:
540-
number = match[0].strip()
541-
agent_text = match[1].strip()
542-
action = match[2].strip()
543-
544-
# Clean up agent name
545-
agent = re.sub(r'\s+', '', agent_text)
546-
if not agent or agent not in self._available_agents:
547-
agent = "GenericAgent" # Default to GenericAgent if not recognized
548-
549-
# Create and store the step
550-
step = Step(
551-
id=str(uuid.uuid4()),
552-
plan_id=plan.id,
553-
session_id=input_task.session_id,
554-
user_id=self._user_id,
555-
action=action,
556-
agent=agent,
557-
status=StepStatus.planned,
558-
human_approval_status=HumanFeedbackStatus.requested
559-
)
560-
561-
await self._memory_store.add_step(step)
562-
steps.append(step)
563-
564-
return plan, steps
427+
# Re-raise the exception to be handled by the calling method
428+
raise
565429

566430
def _generate_instruction(self, objective: str) -> str:
567431
"""Generate instruction for the LLM to create a plan.

0 commit comments

Comments
 (0)