diff --git a/src/backend/app_kernel.py b/src/backend/app_kernel.py index ba7091149..9631b62da 100644 --- a/src/backend/app_kernel.py +++ b/src/backend/app_kernel.py @@ -2,32 +2,34 @@ import asyncio import logging import os + # Azure monitoring import re import uuid -from contextlib import asynccontextmanager + from typing import Dict, List, Optional -from auth.auth_utils import get_authenticated_user_details + from azure.monitor.opentelemetry import configure_azure_monitor from common.config.app_config import config -from common.database.database_factory import DatabaseFactory -from common.models.messages_kernel import (AgentMessage, AgentType, - HumanClarification, HumanFeedback, - InputTask, Plan, PlanStatus, - PlanWithSteps, Step, UserLanguage) -from common.utils.event_utils import track_event_if_configured -from common.utils.utils_date import format_dates_in_messages -# Updated import for KernelArguments -from common.utils.utils_kernel import rai_success +from common.models.messages_kernel import ( + UserLanguage, +) + + # FastAPI imports -from fastapi import (FastAPI, HTTPException, Query, Request, WebSocket, - WebSocketDisconnect) +from fastapi import ( + FastAPI, + Query, + Request, +) from fastapi.middleware.cors import CORSMiddleware -from kernel_agents.agent_factory import AgentFactory + + # Local imports from middleware.health_check import HealthCheckMiddleware from v3.api.router import app_v3 + # Semantic Kernel imports from v3.orchestration.orchestration_manager import OrchestrationManager @@ -112,841 +114,6 @@ async def user_browser_language_endpoint(user_language: UserLanguage, request: R return {"status": "Language received successfully"} -# @app.post("/api/input_task") -# async def input_task_endpoint(input_task: InputTask, request: Request): -# """ -# Receive the initial input task from the user. -# """ -# # Fix 1: Properly await the async rai_success function -# if not await rai_success(input_task.description, True): -# print("RAI failed") - -# track_event_if_configured( -# "RAI failed", -# { -# "status": "Plan not created - RAI validation failed", -# "description": input_task.description, -# "session_id": input_task.session_id, -# }, -# ) - -# return { -# "status": "RAI_VALIDATION_FAILED", -# "message": "Content Safety Check Failed", -# "detail": "Your request contains content that doesn't meet our safety guidelines. Please modify your request to ensure it's appropriate and try again.", -# "suggestions": [ -# "Remove any potentially harmful, inappropriate, or unsafe content", -# "Use more professional and constructive language", -# "Focus on legitimate business or educational objectives", -# "Ensure your request complies with content policies", -# ], -# } -# authenticated_user = get_authenticated_user_details(request_headers=request.headers) -# user_id = authenticated_user["user_principal_id"] - -# if not user_id: -# track_event_if_configured( -# "UserIdNotFound", {"status_code": 400, "detail": "no user"} -# ) -# raise HTTPException(status_code=400, detail="no user") - -# # Generate session ID if not provided -# if not input_task.session_id: -# input_task.session_id = str(uuid.uuid4()) - -# try: -# # Create all agents instead of just the planner agent -# # This ensures other agents are created first and the planner has access to them -# memory_store = await DatabaseFactory.get_database(user_id=user_id) -# client = None -# try: -# client = config.get_ai_project_client() -# except Exception as client_exc: -# logging.error(f"Error creating AIProjectClient: {client_exc}") - -# agents = await AgentFactory.create_all_agents( -# session_id=input_task.session_id, -# user_id=user_id, -# memory_store=memory_store, -# client=client, -# ) - -# group_chat_manager = agents[AgentType.GROUP_CHAT_MANAGER.value] - -# # Convert input task to JSON for the kernel function, add user_id here - -# # Use the planner to handle the task -# await group_chat_manager.handle_input_task(input_task) - -# # Get plan from memory store -# plan = await memory_store.get_plan_by_session(input_task.session_id) - -# if not plan: # If the plan is not found, raise an error -# track_event_if_configured( -# "PlanNotFound", -# { -# "status": "Plan not found", -# "session_id": input_task.session_id, -# "description": input_task.description, -# }, -# ) -# raise HTTPException(status_code=404, detail="Plan not found") -# # Log custom event for successful input task processing -# track_event_if_configured( -# "InputTaskProcessed", -# { -# "status": f"Plan created with ID: {plan.id}", -# "session_id": input_task.session_id, -# "plan_id": plan.id, -# "description": input_task.description, -# }, -# ) -# if client: -# try: -# client.close() -# except Exception as e: -# logging.error(f"Error sending to AIProjectClient: {e}") -# return { -# "status": f"Plan created with ID: {plan.id}", -# "session_id": input_task.session_id, -# "plan_id": plan.id, -# "description": input_task.description, -# } - -# except Exception as e: -# # Extract clean error message for rate limit errors -# error_msg = str(e) -# if "Rate limit is exceeded" in error_msg: -# match = re.search( -# r"Rate limit is exceeded\. Try again in (\d+) seconds?\.", error_msg -# ) -# if match: -# error_msg = "Application temporarily unavailable due to quota limits. Please try again later." - -# track_event_if_configured( -# "InputTaskError", -# { -# "session_id": input_task.session_id, -# "description": input_task.description, -# "error": str(e), -# }, -# ) -# raise HTTPException( -# status_code=400, detail=f"Error creating plan: {error_msg}" -# ) from e - - -@app.post("/api/human_feedback") -async def human_feedback_endpoint(human_feedback: HumanFeedback, request: Request): - """ - Receive human feedback on a step. - - --- - tags: - - Feedback - parameters: - - name: user_principal_id - in: header - type: string - required: true - description: User ID extracted from the authentication header - - name: body - in: body - required: true - schema: - type: object - properties: - step_id: - type: string - description: The ID of the step to provide feedback for - plan_id: - type: string - description: The plan ID - session_id: - type: string - description: The session ID - approved: - type: boolean - description: Whether the step is approved - human_feedback: - type: string - description: Optional feedback details - updated_action: - type: string - description: Optional updated action - user_id: - type: string - description: The user ID providing the feedback - responses: - 200: - description: Feedback received successfully - schema: - type: object - properties: - status: - type: string - session_id: - type: string - step_id: - type: string - 400: - description: Missing or invalid user information - """ - authenticated_user = get_authenticated_user_details(request_headers=request.headers) - user_id = authenticated_user["user_principal_id"] - if not user_id: - track_event_if_configured( - "UserIdNotFound", {"status_code": 400, "detail": "no user"} - ) - raise HTTPException(status_code=400, detail="no user") - - memory_store = await DatabaseFactory.get_database(user_id=user_id) - - client = None - try: - client = config.get_ai_project_client() - except Exception as client_exc: - logging.error(f"Error creating AIProjectClient: {client_exc}") - - human_agent = await AgentFactory.create_agent( - agent_type=AgentType.HUMAN, - session_id=human_feedback.session_id, - user_id=user_id, - memory_store=memory_store, - client=client, - ) - - if human_agent is None: - track_event_if_configured( - "AgentNotFound", - { - "status": "Agent not found", - "session_id": human_feedback.session_id, - "step_id": human_feedback.step_id, - }, - ) - raise HTTPException(status_code=404, detail="Agent not found") - - # Use the human agent to handle the feedback - await human_agent.handle_human_feedback(human_feedback=human_feedback) - - track_event_if_configured( - "Completed Feedback received", - { - "status": "Feedback received", - "session_id": human_feedback.session_id, - "step_id": human_feedback.step_id, - }, - ) - if client: - try: - client.close() - except Exception as e: - logging.error(f"Error sending to AIProjectClient: {e}") - return { - "status": "Feedback received", - "session_id": human_feedback.session_id, - "step_id": human_feedback.step_id, - } - - -@app.post("/api/human_clarification_on_plan") -async def human_clarification_endpoint( - human_clarification: HumanClarification, request: Request -): - """ - Receive human clarification on a plan. - - --- - tags: - - Clarification - parameters: - - name: user_principal_id - in: header - type: string - required: true - description: User ID extracted from the authentication header - - name: body - in: body - required: true - schema: - type: object - properties: - plan_id: - type: string - description: The plan ID requiring clarification - session_id: - type: string - description: The session ID - human_clarification: - type: string - description: Clarification details provided by the user - user_id: - type: string - description: The user ID providing the clarification - responses: - 200: - description: Clarification received successfully - schema: - type: object - properties: - status: - type: string - session_id: - type: string - 400: - description: Missing or invalid user information - """ - if not await rai_success(human_clarification.human_clarification, False): - print("RAI failed") - track_event_if_configured( - "RAI failed", - { - "status": "Clarification rejected - RAI validation failed", - "description": human_clarification.human_clarification, - "session_id": human_clarification.session_id, - }, - ) - raise HTTPException( - status_code=400, - detail={ - "error_type": "RAI_VALIDATION_FAILED", - "message": "Clarification Safety Check Failed", - "description": "Your clarification contains content that doesn't meet our safety guidelines. Please provide a more appropriate clarification.", - "suggestions": [ - "Use clear and professional language", - "Avoid potentially harmful or inappropriate content", - "Focus on providing constructive feedback or clarification", - "Ensure your message complies with content policies", - ], - "user_action": "Please revise your clarification and try again", - }, - ) - - authenticated_user = get_authenticated_user_details(request_headers=request.headers) - user_id = authenticated_user["user_principal_id"] - if not user_id: - track_event_if_configured( - "UserIdNotFound", {"status_code": 400, "detail": "no user"} - ) - raise HTTPException(status_code=400, detail="no user") - - memory_store = await DatabaseFactory.get_database(user_id=user_id) - client = None - try: - client = config.get_ai_project_client() - except Exception as client_exc: - logging.error(f"Error creating AIProjectClient: {client_exc}") - - human_agent = await AgentFactory.create_agent( - agent_type=AgentType.HUMAN, - session_id=human_clarification.session_id, - user_id=user_id, - memory_store=memory_store, - client=client, - ) - - if human_agent is None: - track_event_if_configured( - "AgentNotFound", - { - "status": "Agent not found", - "session_id": human_clarification.session_id, - "step_id": human_clarification.step_id, - }, - ) - raise HTTPException(status_code=404, detail="Agent not found") - - # Use the human agent to handle the feedback - await human_agent.handle_human_clarification( - human_clarification=human_clarification - ) - - track_event_if_configured( - "Completed Human clarification on the plan", - { - "status": "Clarification received", - "session_id": human_clarification.session_id, - }, - ) - if client: - try: - client.close() - except Exception as e: - logging.error(f"Error sending to AIProjectClient: {e}") - return { - "status": "Clarification received", - "session_id": human_clarification.session_id, - } - - -@app.post("/api/approve_step_or_steps") -async def approve_step_endpoint( - human_feedback: HumanFeedback, request: Request -) -> Dict[str, str]: - """ - Approve a step or multiple steps in a plan. - - --- - tags: - - Approval - parameters: - - name: user_principal_id - in: header - type: string - required: true - description: User ID extracted from the authentication header - - name: body - in: body - required: true - schema: - type: object - properties: - step_id: - type: string - description: Optional step ID to approve - plan_id: - type: string - description: The plan ID - session_id: - type: string - description: The session ID - approved: - type: boolean - description: Whether the step(s) are approved - human_feedback: - type: string - description: Optional feedback details - updated_action: - type: string - description: Optional updated action - user_id: - type: string - description: The user ID providing the approval - responses: - 200: - description: Approval status returned - schema: - type: object - properties: - status: - type: string - 400: - description: Missing or invalid user information - """ - authenticated_user = get_authenticated_user_details(request_headers=request.headers) - user_id = authenticated_user["user_principal_id"] - if not user_id: - track_event_if_configured( - "UserIdNotFound", {"status_code": 400, "detail": "no user"} - ) - raise HTTPException(status_code=400, detail="no user") - - # Get the agents for this session - memory_store = await DatabaseFactory.get_database(user_id=user_id) - client = None - try: - client = config.get_ai_project_client() - except Exception as client_exc: - logging.error(f"Error creating AIProjectClient: {client_exc}") - agents = await AgentFactory.create_all_agents( - session_id=human_feedback.session_id, - user_id=user_id, - memory_store=memory_store, - client=client, - ) - - # Send the approval to the group chat manager - group_chat_manager = agents[AgentType.GROUP_CHAT_MANAGER.value] - - await group_chat_manager.handle_human_feedback(human_feedback) - - if client: - try: - client.close() - except Exception as e: - logging.error(f"Error sending to AIProjectClient: {e}") - # Return a status message - if human_feedback.step_id: - track_event_if_configured( - "Completed Human clarification with step_id", - { - "status": f"Step {human_feedback.step_id} - Approval:{human_feedback.approved}." - }, - ) - - return { - "status": f"Step {human_feedback.step_id} - Approval:{human_feedback.approved}." - } - else: - track_event_if_configured( - "Completed Human clarification without step_id", - {"status": "All steps approved"}, - ) - - return {"status": "All steps approved"} - - -# Get plans is called in the initial side rendering of the frontend -@app.get("/api/plans") -async def get_plans( - request: Request, - plan_id: Optional[str] = Query(None), -): - """ - Retrieve plans for the current user. - - --- - tags: - - Plans - parameters: - - name: session_id - in: query - type: string - required: false - description: Optional session ID to retrieve plans for a specific session - responses: - 200: - description: List of plans with steps for the user - schema: - type: array - items: - type: object - properties: - id: - type: string - description: Unique ID of the plan - session_id: - type: string - description: Session ID associated with the plan - initial_goal: - type: string - description: The initial goal derived from the user's input - overall_status: - type: string - description: Status of the plan (e.g., in_progress, completed) - steps: - type: array - items: - type: object - properties: - id: - type: string - description: Unique ID of the step - plan_id: - type: string - description: ID of the plan the step belongs to - action: - type: string - description: The action to be performed - agent: - type: string - description: The agent responsible for the step - status: - type: string - description: Status of the step (e.g., planned, approved, completed) - 400: - description: Missing or invalid user information - 404: - description: Plan not found - """ - - authenticated_user = get_authenticated_user_details(request_headers=request.headers) - user_id = authenticated_user["user_principal_id"] - if not user_id: - track_event_if_configured( - "UserIdNotFound", {"status_code": 400, "detail": "no user"} - ) - raise HTTPException(status_code=400, detail="no user") - - #### Replace the following with code to get plan run history from the database - - # # Initialize memory context - memory_store = await DatabaseFactory.get_database(user_id=user_id) - - if plan_id: - plan = await memory_store.get_plan_by_plan_id(plan_id=plan_id) - if not plan: - track_event_if_configured( - "GetPlanBySessionNotFound", - {"status_code": 400, "detail": "Plan not found"}, - ) - raise HTTPException(status_code=404, detail="Plan not found") - - # Use get_steps_by_plan to match the original implementation - steps = await memory_store.get_steps_by_plan(plan_id=plan.id) - messages = await memory_store.get_data_by_type_and_session_id( - "agent_message", session_id=plan.session_id - ) - - plan_with_steps = PlanWithSteps(**plan.model_dump(), steps=steps) - plan_with_steps.update_step_counts() - - # Format dates in messages according to locale - formatted_messages = format_dates_in_messages( - messages, config.get_user_local_browser_language() - ) - - return [plan_with_steps, formatted_messages] - - current_team = await memory_store.get_current_team(user_id=user_id) - if not current_team: - return [] - - all_plans = await memory_store.get_all_plans_by_team_id(team_id=current_team.id) - # Fetch steps for all plans concurrently - steps_for_all_plans = await asyncio.gather( - *[memory_store.get_steps_by_plan(plan_id=plan.id) for plan in all_plans] - ) - # Create list of PlanWithSteps and update step counts - list_of_plans_with_steps = [] - for plan, steps in zip(all_plans, steps_for_all_plans): - plan_with_steps = PlanWithSteps(**plan.model_dump(), steps=steps) - plan_with_steps.update_step_counts() - list_of_plans_with_steps.append(plan_with_steps) - - return [] - - -@app.get("/api/steps/{plan_id}", response_model=List[Step]) -async def get_steps_by_plan(plan_id: str, request: Request) -> List[Step]: - """ - Retrieve steps for a specific plan. - - --- - tags: - - Steps - parameters: - - name: plan_id - in: path - type: string - required: true - description: The ID of the plan to retrieve steps for - responses: - 200: - description: List of steps associated with the specified plan - schema: - type: array - items: - type: object - properties: - id: - type: string - description: Unique ID of the step - plan_id: - type: string - description: ID of the plan the step belongs to - action: - type: string - description: The action to be performed - agent: - type: string - description: The agent responsible for the step - status: - type: string - description: Status of the step (e.g., planned, approved, completed) - agent_reply: - type: string - description: Optional response from the agent after execution - human_feedback: - type: string - description: Optional feedback provided by a human - updated_action: - type: string - description: Optional modified action based on feedback - 400: - description: Missing or invalid user information - 404: - description: Plan or steps not found - """ - authenticated_user = get_authenticated_user_details(request_headers=request.headers) - user_id = authenticated_user["user_principal_id"] - if not user_id: - track_event_if_configured( - "UserIdNotFound", {"status_code": 400, "detail": "no user"} - ) - raise HTTPException(status_code=400, detail="no user") - - # Initialize memory context - memory_store = await DatabaseFactory.get_database(user_id=user_id) - steps = await memory_store.get_steps_for_plan(plan_id=plan_id) - return steps - - -@app.get("/api/agent_messages/{session_id}", response_model=List[AgentMessage]) -async def get_agent_messages(session_id: str, request: Request) -> List[AgentMessage]: - """ - Retrieve agent messages for a specific session. - - --- - tags: - - Agent Messages - parameters: - - name: session_id - in: path - type: string - required: true - in: path - type: string - required: true - description: The ID of the session to retrieve agent messages for - responses: - 200: - description: List of agent messages associated with the specified session - schema: - type: array - items: - type: object - properties: - id: - type: string - description: Unique ID of the agent message - session_id: - type: string - description: Session ID associated with the message - plan_id: - type: string - description: Plan ID related to the agent message - content: - type: string - description: Content of the message - source: - type: string - description: Source of the message (e.g., agent type) - timestamp: - type: string - format: date-time - description: Timestamp of the message - step_id: - type: string - description: Optional step ID associated with the message - 400: - description: Missing or invalid user information - 404: - description: Agent messages not found - """ - authenticated_user = get_authenticated_user_details(request_headers=request.headers) - user_id = authenticated_user["user_principal_id"] - if not user_id: - track_event_if_configured( - "UserIdNotFound", {"status_code": 400, "detail": "no user"} - ) - raise HTTPException(status_code=400, detail="no user") - - # Initialize memory context - memory_store = await DatabaseFactory.get_database(user_id=user_id) - agent_messages = await memory_store.get_data_by_type("agent_message") - return agent_messages - - -@app.get("/api/agent-tools") -async def get_agent_tools(): - """ - Retrieve all available agent tools. - - --- - tags: - - Agent Tools - responses: - 200: - description: List of all available agent tools and their descriptions - schema: - type: array - items: - type: object - properties: - agent: - type: string - description: Name of the agent associated with the tool - function: - type: string - description: Name of the tool function - description: - type: string - description: Detailed description of what the tool does - arguments: - type: string - description: Arguments required by the tool function - """ - return [] - - -# @app.post("/api/test/streaming/{plan_id}") -# async def test_streaming_updates(plan_id: str): -# """ -# Test endpoint to simulate streaming updates for a plan. -# This is for testing the WebSocket streaming functionality. -# """ -# from common.utils.websocket_streaming import (send_agent_message, -# send_plan_update, -# send_step_update) - -# try: -# # Simulate a series of streaming updates -# await send_agent_message( -# plan_id=plan_id, -# agent_name="Data Analyst", -# content="Starting analysis of the data...", -# message_type="thinking", -# ) - -# await asyncio.sleep(1) - -# await send_plan_update( -# plan_id=plan_id, -# step_id="step_1", -# agent_name="Data Analyst", -# content="Analyzing customer data patterns...", -# status="in_progress", -# message_type="action", -# ) - -# await asyncio.sleep(2) - -# await send_agent_message( -# plan_id=plan_id, -# agent_name="Data Analyst", -# content="Found 3 key insights in the customer data. Processing recommendations...", -# message_type="result", -# ) - -# await asyncio.sleep(1) - -# await send_step_update( -# plan_id=plan_id, -# step_id="step_1", -# status="completed", -# content="Data analysis completed successfully!", -# ) - -# await send_agent_message( -# plan_id=plan_id, -# agent_name="Business Advisor", -# content="Reviewing the analysis results and preparing strategic recommendations...", -# message_type="thinking", -# ) - -# await asyncio.sleep(2) - -# await send_plan_update( -# plan_id=plan_id, -# step_id="step_2", -# agent_name="Business Advisor", -# content="Based on the data analysis, I recommend focusing on customer retention strategies for the identified high-value segments.", -# status="completed", -# message_type="result", -# ) - -# return { -# "status": "success", -# "message": f"Test streaming updates sent for plan {plan_id}", -# } - -# except Exception as e: -# logging.error(f"Error sending test streaming updates: {e}") -# raise HTTPException(status_code=500, detail=str(e)) - - # Run the app if __name__ == "__main__": import uvicorn diff --git a/src/backend/common/models/messages_kernel.py b/src/backend/common/models/messages_kernel.py index 19abbb4cc..50c3bf1d7 100644 --- a/src/backend/common/models/messages_kernel.py +++ b/src/backend/common/models/messages_kernel.py @@ -12,8 +12,11 @@ class DataType(str, Enum): session = "session" plan = "plan" step = "step" - message = "message" - team = "team" + message = "agent_message" + team = "team_config" + user_current_team = "user_current_team" + m_plan = "m_plan" + m_plan_step = "m_plan_step" class AgentType(str, Enum): @@ -98,6 +101,7 @@ class Session(BaseDataModel): current_status: str message_to_user: Optional[str] = None + class UserCurrentTeam(BaseDataModel): """Represents the current team of a user.""" @@ -105,11 +109,12 @@ class UserCurrentTeam(BaseDataModel): user_id: str team_id: str + class Plan(BaseDataModel): """Represents a plan containing multiple steps.""" data_type: Literal["plan"] = Field("plan", Literal=True) - plan_id: str + plan_id: str session_id: str user_id: str initial_goal: str @@ -119,7 +124,7 @@ class Plan(BaseDataModel): team_id: Optional[str] = None human_clarification_request: Optional[str] = None human_clarification_response: Optional[str] = None - + class Step(BaseDataModel): """Represents an individual step (task) within a plan.""" @@ -137,24 +142,10 @@ class Step(BaseDataModel): updated_action: Optional[str] = None -class ThreadIdAgent(BaseDataModel): - """Represents an individual thread_id.""" - - data_type: Literal["thread"] = Field("thread", Literal=True) - session_id: str # Partition key - user_id: str - thread_id: str - - -class AzureIdAgent(BaseDataModel): - """Represents an individual thread_id.""" +class TeamSelectionRequest(KernelBaseModel): + """Request model for team selection.""" - data_type: Literal["agent"] = Field("agent", Literal=True) - session_id: str # Partition key - user_id: str - action: str - agent: AgentType - agent_id: str + team_id: str class TeamAgent(KernelBaseModel): @@ -185,9 +176,6 @@ class StartingTask(KernelBaseModel): creator: str logo: str -class TeamSelectionRequest(KernelBaseModel): - """Request model for team selection.""" - team_id: str class TeamConfiguration(BaseDataModel): """Represents a team configuration stored in the database.""" @@ -260,80 +248,3 @@ class InputTask(KernelBaseModel): class UserLanguage(KernelBaseModel): language: str - - -class GeneratePlanRequest(KernelBaseModel): - """Message representing a request to generate a plan from an existing plan ID.""" - - plan_id: str - - -class ApprovalRequest(KernelBaseModel): - """Message sent to HumanAgent to request approval for a step.""" - - step_id: str - plan_id: str - session_id: str - user_id: str - action: str - agent: AgentType - - -class HumanFeedback(KernelBaseModel): - """Message containing human feedback on a step.""" - - step_id: Optional[str] = None - plan_id: str - session_id: str - approved: bool - human_feedback: Optional[str] = None - updated_action: Optional[str] = None - - -class HumanClarification(KernelBaseModel): - """Message containing human clarification on a plan.""" - - plan_id: str - session_id: str - human_clarification: str - - -class ActionRequest(KernelBaseModel): - """Message sent to an agent to perform an action.""" - - step_id: str - plan_id: str - session_id: str - action: str - agent: AgentType - - -class ActionResponse(KernelBaseModel): - """Message containing the response from an agent after performing an action.""" - - step_id: str - plan_id: str - session_id: str - result: str - status: StepStatus # Should be 'completed' or 'failed' - - -class PlanStateUpdate(KernelBaseModel): - """Optional message for updating the plan state.""" - - plan_id: str - session_id: str - overall_status: PlanStatus - - -# Define the expected structure of the LLM response -class PlannerResponseStep(KernelBaseModel): - action: str - agent: AgentType - - -class PlannerResponsePlan(KernelBaseModel): - initial_goal: str - steps: List[PlannerResponseStep] - summary_plan_and_steps: str - human_clarification_request: Optional[str] = None diff --git a/src/backend/common/services/__init__.py b/src/backend/common/services/__init__.py deleted file mode 100644 index a70b3029a..000000000 --- a/src/backend/common/services/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Services package diff --git a/src/backend/kernel_agents/agent_base.py b/src/backend/kernel_agents/agent_base.py deleted file mode 100644 index 24e9b1244..000000000 --- a/src/backend/kernel_agents/agent_base.py +++ /dev/null @@ -1,324 +0,0 @@ -import logging -from abc import abstractmethod -from typing import Any, List, Mapping, Optional - -# Import the new AppConfig instance -from common.config.app_config import config - -from common.utils.event_utils import track_event_if_configured -from common.models.messages_kernel import ( - ActionRequest, - ActionResponse, - AgentMessage, - Step, - StepStatus, -) -from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent -from semantic_kernel.functions import KernelFunction - -from common.database.database_base import DatabaseBase - -# Default formatting instructions used across agents -DEFAULT_FORMATTING_INSTRUCTIONS = "Instructions: returning the output of this function call verbatim to the user in markdown. Then write AGENT SUMMARY: and then include a summary of what you did." - - -class BaseAgent(AzureAIAgent): - """BaseAgent implemented using Semantic Kernel with Azure AI Agent support.""" - - def __init__( - self, - agent_name: str, - session_id: str, - user_id: str, - memory_store: Optional[DatabaseBase] = None, - tools: Optional[List[KernelFunction]] = None, - system_message: Optional[str] = None, - client=None, - definition=None, - ): - """Initialize the base agent. - - Args: - agent_name: The name of the agent - session_id: The session ID - user_id: The user ID - memory_store: The memory context for storing agent state - tools: Optional list of tools for the agent - system_message: Optional system message for the agent - agent_type: Optional agent type string for automatic tool loading - client: The client required by AzureAIAgent - definition: The definition required by AzureAIAgent - """ - - tools = tools or [] - system_message = system_message or self.default_system_message(agent_name) - - # Call AzureAIAgent constructor with required client and definition - super().__init__( - deployment_name=None, # Set as needed - plugins=tools, # Use the loaded plugins, - endpoint=None, # Set as needed - api_version=None, # Set as needed - token=None, # Set as needed - model=config.AZURE_OPENAI_DEPLOYMENT_NAME, - agent_name=agent_name, - system_prompt=system_message, - client=client, - definition=definition, - ) - - # Store instance variables - self._agent_name = agent_name - self._session_id = session_id - self._user_id = user_id - self._memory_store = memory_store - self._tools = tools - self._system_message = system_message - self._chat_history = [{"role": "system", "content": self._system_message}] - # self._agent = None # Will be initialized in async_init - - # Required properties for AgentGroupChat compatibility - self.name = agent_name # This is crucial for AgentGroupChat to identify agents - - # @property - # def plugins(self) -> Optional[dict[str, Callable]]: - # """Get the plugins for this agent. - - # Returns: - # A list of plugins, or None if not applicable. - # """ - # return None - @staticmethod - def default_system_message(agent_name=None) -> str: - name = agent_name - return f"You are an AI assistant named {name}. Help the user by providing accurate and helpful information." - - async def handle_action_request(self, action_request: ActionRequest) -> str: - """Handle an action request from another agent or the system. - - Args: - action_request_json: The action request as a JSON string - - Returns: - A JSON string containing the action response - """ - - # Get the step from memory - step: Step = await self._memory_store.get_step( - action_request.step_id, action_request.session_id - ) - - if not step: - # Create error response if step not found - response = ActionResponse( - step_id=action_request.step_id, - status=StepStatus.failed, - message="Step not found in memory.", - ) - return response.json() - - # Add messages to chat history for context - # This gives the agent visibility of the conversation history - self._chat_history.extend( - [ - {"role": "assistant", "content": action_request.action}, - { - "role": "user", - "content": f"{step.human_feedback}. Now make the function call", - }, - ] - ) - - try: - # Use the agent to process the action - # chat_history = self._chat_history.copy() - - # Call the agent to handle the action - thread = None - # thread = self.client.agents.get_thread( - # thread=step.session_id - # ) # AzureAIAgentThread(thread_id=step.session_id) - async_generator = self.invoke( - messages=f"{str(self._chat_history)}\n\nPlease perform this action : {step.action}", - thread=thread, - ) - - response_content = "" - - # Collect the response from the async generator - async for chunk in async_generator: - if chunk is not None: - response_content += str(chunk) - - logging.info(f"Response content length: {len(response_content)}") - logging.info(f"Response content: {response_content}") - - # Store agent message in cosmos memory - await self._memory_store.add_item( - AgentMessage( - session_id=action_request.session_id, - user_id=self._user_id, - plan_id=action_request.plan_id, - content=f"{response_content}", - source=self._agent_name, - step_id=action_request.step_id, - ) - ) - - # Track telemetry - track_event_if_configured( - "Base agent - Added into the cosmos", - { - "session_id": action_request.session_id, - "user_id": self._user_id, - "plan_id": action_request.plan_id, - "content": f"{response_content}", - "source": self._agent_name, - "step_id": action_request.step_id, - }, - ) - - except Exception as e: - logging.exception(f"Error during agent execution: {e}") - - # Track error in telemetry - track_event_if_configured( - "Base agent - Error during agent execution, captured into the cosmos", - { - "session_id": action_request.session_id, - "user_id": self._user_id, - "plan_id": action_request.plan_id, - "content": f"{e}", - "source": self._agent_name, - "step_id": action_request.step_id, - }, - ) - - # Return an error response - response = ActionResponse( - step_id=action_request.step_id, - plan_id=action_request.plan_id, - session_id=action_request.session_id, - result=f"Error: {str(e)}", - status=StepStatus.failed, - ) - return response.json() - - # Update step status - step.status = StepStatus.completed - step.agent_reply = response_content - await self._memory_store.update_step(step) - - # Track step completion in telemetry - track_event_if_configured( - "Base agent - Updated step and updated into the cosmos", - { - "status": StepStatus.completed, - "session_id": action_request.session_id, - "agent_reply": f"{response_content}", - "user_id": self._user_id, - "plan_id": action_request.plan_id, - "content": f"{response_content}", - "source": self._agent_name, - "step_id": action_request.step_id, - }, - ) - - # Create and return action response - response = ActionResponse( - step_id=step.id, - plan_id=step.plan_id, - session_id=action_request.session_id, - result=response_content, - status=StepStatus.completed, - ) - - return response.json() - - def save_state(self) -> Mapping[str, Any]: - """Save the state of this agent.""" - return {"memory": self._memory_store.save_state()} - - def load_state(self, state: Mapping[str, Any]) -> None: - """Load the state of this agent.""" - self._memory_store.load_state(state["memory"]) - - @classmethod - @abstractmethod - async def create(cls, **kwargs) -> "BaseAgent": - """Create an instance of the agent.""" - pass - - @staticmethod - async def _create_azure_ai_agent_definition( - agent_name: str, - instructions: str, - tools: Optional[List[KernelFunction]] = None, - client=None, - response_format=None, - temperature: float = 0.0, - ): - """ - Creates a new Azure AI Agent with the specified name and instructions using AIProjectClient. - If an agent with the given name (assistant_id) already exists, it tries to retrieve it first. - - Args: - kernel: The Semantic Kernel instance - agent_name: The name of the agent (will be used as assistant_id) - instructions: The system message / instructions for the agent - agent_type: The type of agent (defaults to "assistant") - tools: Optional tool definitions for the agent - tool_resources: Optional tool resources required by the tools - response_format: Optional response format to control structured output - temperature: The temperature setting for the agent (defaults to 0.0) - - Returns: - A new AzureAIAgent definition or an existing one if found - """ - try: - # Get the AIProjectClient - if client is None: - client = config.get_ai_project_client() - - # # First try to get an existing agent with this name as assistant_id - try: - agent_id = None - agent_list = client.agents.list_agents() - async for agent in agent_list: - if agent.name == agent_name: - agent_id = agent.id - break - # If the agent already exists, we can use it directly - # Get the existing agent definition - if agent_id is not None: - logging.info(f"Agent with ID {agent_id} exists.") - - existing_definition = await client.agents.get_agent(agent_id) - - return existing_definition - except Exception as e: - # The Azure AI Projects SDK throws an exception when the agent doesn't exist - # (not returning None), so we catch it and proceed to create a new agent - if "ResourceNotFound" in str(e) or "404" in str(e): - logging.info( - f"Agent with ID {agent_name} not found. Will create a new one." - ) - else: - # Log unexpected errors but still try to create a new agent - logging.warning( - f"Unexpected error while retrieving agent {agent_name}: {str(e)}. Attempting to create new agent." - ) - - # Create the agent using the project client with the agent_name as both name and assistantId - agent_definition = await client.agents.create_agent( - model=config.AZURE_OPENAI_DEPLOYMENT_NAME, - name=agent_name, - instructions=instructions, - temperature=temperature, - response_format=response_format, - ) - - return agent_definition - except Exception as exc: - logging.error("Failed to create Azure AI Agent: %s", exc) - raise diff --git a/src/backend/kernel_agents/agent_factory.py b/src/backend/kernel_agents/agent_factory.py deleted file mode 100644 index 0ec0480c4..000000000 --- a/src/backend/kernel_agents/agent_factory.py +++ /dev/null @@ -1,335 +0,0 @@ -"""Factory for creating agents in the Multi-Agent Custom Automation Engine.""" - -import inspect -import logging -from typing import Any, Dict, Optional, Type - -# Import the new AppConfig instance -from common.config.app_config import config -from azure.ai.agents.models import ( - ResponseFormatJsonSchema, - ResponseFormatJsonSchemaType, -) -from kernel_agents.agent_base import BaseAgent -from kernel_agents.generic_agent import GenericAgent -from kernel_agents.group_chat_manager import GroupChatManager - -# Import all specialized agent implementations -from kernel_agents.hr_agent import HrAgent -from kernel_agents.human_agent import HumanAgent -from kernel_agents.marketing_agent import MarketingAgent -from kernel_agents.planner_agent import PlannerAgent # Add PlannerAgent import -from kernel_agents.procurement_agent import ProcurementAgent -from kernel_agents.product_agent import ProductAgent -from kernel_agents.tech_support_agent import TechSupportAgent -from common.models.messages_kernel import AgentType, PlannerResponsePlan - -# pylint:disable=E0611 -from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent - -from common.database.database_base import DatabaseBase -from common.database.database_factory import DatabaseFactory - -logger = logging.getLogger(__name__) - - -class AgentFactory: - """Factory for creating agents in the Multi-Agent Custom Automation Engine.""" - - # Mapping of agent types to their implementation classes - _agent_classes: Dict[AgentType, Type[BaseAgent]] = { - AgentType.HR: HrAgent, - AgentType.MARKETING: MarketingAgent, - AgentType.PRODUCT: ProductAgent, - AgentType.PROCUREMENT: ProcurementAgent, - AgentType.TECH_SUPPORT: TechSupportAgent, - AgentType.GENERIC: GenericAgent, - AgentType.HUMAN: HumanAgent, - AgentType.PLANNER: PlannerAgent, - AgentType.GROUP_CHAT_MANAGER: GroupChatManager, # Add GroupChatManager - } - - # Mapping of agent types to their string identifiers (for automatic tool loading) - _agent_type_strings: Dict[AgentType, str] = { - AgentType.HR: AgentType.HR.value, - AgentType.MARKETING: AgentType.MARKETING.value, - AgentType.PRODUCT: AgentType.PRODUCT.value, - AgentType.PROCUREMENT: AgentType.PROCUREMENT.value, - AgentType.TECH_SUPPORT: AgentType.TECH_SUPPORT.value, - AgentType.GENERIC: AgentType.GENERIC.value, - AgentType.HUMAN: AgentType.HUMAN.value, - AgentType.PLANNER: AgentType.PLANNER.value, - AgentType.GROUP_CHAT_MANAGER: AgentType.GROUP_CHAT_MANAGER.value, - } - - # System messages for each agent type - _agent_system_messages: Dict[AgentType, str] = { - AgentType.HR: HrAgent.default_system_message(), - AgentType.MARKETING: MarketingAgent.default_system_message(), - AgentType.PRODUCT: ProductAgent.default_system_message(), - AgentType.PROCUREMENT: ProcurementAgent.default_system_message(), - AgentType.TECH_SUPPORT: TechSupportAgent.default_system_message(), - AgentType.GENERIC: GenericAgent.default_system_message(), - AgentType.HUMAN: HumanAgent.default_system_message(), - AgentType.PLANNER: PlannerAgent.default_system_message(), - AgentType.GROUP_CHAT_MANAGER: GroupChatManager.default_system_message(), - } - - # Cache of agent instances by session_id and agent_type - _agent_cache: Dict[str, Dict[AgentType, BaseAgent]] = {} - - # Cache of Azure AI Agent instances - _azure_ai_agent_cache: Dict[str, Dict[str, AzureAIAgent]] = {} - - @classmethod - async def create_agent( - cls, - agent_type: AgentType, - session_id: str, - user_id: str, - temperature: float = 0.0, - memory_store: Optional[DatabaseBase] = None, - system_message: Optional[str] = None, - response_format: Optional[Any] = None, - client: Optional[Any] = None, - **kwargs, - ) -> BaseAgent: - """Create an agent of the specified type. - - This method creates and initializes an agent instance of the specified type. If an agent - of the same type already exists for the session, it returns the cached instance. The method - handles the complete initialization process including: - 1. Creating a memory store for the agent - 2. Setting up the Semantic Kernel - 3. Loading appropriate tools from JSON configuration files - 4. Creating an Azure AI agent definition using the AI Project client - 5. Initializing the agent with all required parameters - 6. Running any asynchronous initialization if needed - 7. Caching the agent for future use - - Args: - agent_type: The type of agent to create (from AgentType enum) - session_id: The unique identifier for the current session - user_id: The user identifier for the current user - temperature: The temperature parameter for the agent's responses (0.0-1.0) - system_message: Optional custom system message to override default - response_format: Optional response format configuration for structured outputs - **kwargs: Additional parameters to pass to the agent constructor - - Returns: - An initialized instance of the specified agent type - - Raises: - ValueError: If the agent type is unknown or initialization fails - """ - # Check if we already have an agent in the cache - if ( - session_id in cls._agent_cache - and agent_type in cls._agent_cache[session_id] - ): - logger.info( - f"Returning cached agent instance for session {session_id} and agent type {agent_type}" - ) - return cls._agent_cache[session_id][agent_type] - - # Get the agent class - agent_class = cls._agent_classes.get(agent_type) - if not agent_class: - raise ValueError(f"Unknown agent type: {agent_type}") - - # Create memory store - if memory_store is None: - memory_store = await DatabaseFactory.get_database(user_id=user_id) - - # Use default system message if none provided - if system_message is None: - system_message = cls._agent_system_messages.get( - agent_type, - f"You are a helpful AI assistant specialized in {cls._agent_type_strings.get(agent_type, 'general')} tasks.", - ) - - # For other agent types, use the standard tool loading mechanism - agent_type_str = cls._agent_type_strings.get( - agent_type, agent_type.value.lower() - ) - tools = None - - # Create the agent instance using the project-based pattern - try: - # Filter kwargs to only those accepted by the agent's __init__ - agent_init_params = inspect.signature(agent_class.__init__).parameters - valid_keys = set(agent_init_params.keys()) - {"self"} - filtered_kwargs = { - k: v - for k, v in { - "agent_name": agent_type_str, - "session_id": session_id, - "user_id": user_id, - "memory_store": memory_store, - "tools": tools, - "system_message": system_message, - "client": client, - **kwargs, - }.items() - if k in valid_keys - } - agent = await agent_class.create(**filtered_kwargs) - - except Exception as e: - logger.error( - f"Error creating agent of type {agent_type} with parameters: {e}" - ) - raise - - # Cache the agent instance - if session_id not in cls._agent_cache: - cls._agent_cache[session_id] = {} - cls._agent_cache[session_id][agent_type] = agent - - return agent - - @classmethod - async def create_all_agents( - cls, - session_id: str, - user_id: str, - temperature: float = 0.0, - memory_store: Optional[DatabaseBase] = None, - client: Optional[Any] = None, - ) -> Dict[AgentType, BaseAgent]: - """Create all agent types for a session in a specific order. - - This method creates all agent instances for a session in a multi-phase approach: - 1. First, it creates all basic agent types except for the Planner and GroupChatManager - 2. Then it creates the Planner agent, providing it with references to all other agents - 3. Finally, it creates the GroupChatManager with references to all agents including the Planner - - This ordered creation ensures that dependencies between agents are properly established, - particularly for the Planner and GroupChatManager which need to coordinate other agents. - - Args: - session_id: The unique identifier for the current session - user_id: The user identifier for the current user - temperature: The temperature parameter for agent responses (0.0-1.0) - - Returns: - Dictionary mapping agent types (from AgentType enum) to initialized agent instances - """ - - # Create each agent type in two phases - # First, create all agents except PlannerAgent and GroupChatManager - agents = {} - planner_agent_type = AgentType.PLANNER - group_chat_manager_type = AgentType.GROUP_CHAT_MANAGER - - try: - if client is None: - # Create the AIProjectClient instance using the config - # This is a placeholder; replace with actual client creation logic - client = config.get_ai_project_client() - except Exception as client_exc: - logger.error(f"Error creating AIProjectClient: {client_exc}") - # Initialize cache for this session if it doesn't exist - if session_id not in cls._agent_cache: - cls._agent_cache[session_id] = {} - - # Phase 1: Create all agents except planner and group chat manager - for agent_type in [ - at - for at in cls._agent_classes.keys() - if at != planner_agent_type and at != group_chat_manager_type - ]: - agents[agent_type] = await cls.create_agent( - agent_type=agent_type, - session_id=session_id, - user_id=user_id, - temperature=temperature, - client=client, - memory_store=memory_store, - ) - - # Create agent name to instance mapping for the planner - agent_instances = {} - for agent_type, agent in agents.items(): - agent_name = agent_type.value - - logging.info( - f"Creating agent instance for {agent_name} with type {agent_type}" - ) - agent_instances[agent_name] = agent - - # Log the agent instances for debugging - logger.info( - f"Created {len(agent_instances)} agent instances for planner: {', '.join(agent_instances.keys())}" - ) - - # Phase 2: Create the planner agent with agent_instances - planner_agent = await cls.create_agent( - agent_type=AgentType.PLANNER, - session_id=session_id, - user_id=user_id, - temperature=temperature, - agent_instances=agent_instances, # Pass agent instances to the planner - client=client, - response_format=ResponseFormatJsonSchemaType( - json_schema=ResponseFormatJsonSchema( - name=PlannerResponsePlan.__name__, - description=f"respond with {PlannerResponsePlan.__name__.lower()}", - schema=PlannerResponsePlan.model_json_schema(), - ) - ), - ) - agent_instances[AgentType.PLANNER.value] = ( - planner_agent # to pass it to group chat manager - ) - agents[planner_agent_type] = planner_agent - - # Phase 3: Create group chat manager with all agents including the planner - group_chat_manager = await cls.create_agent( - agent_type=AgentType.GROUP_CHAT_MANAGER, - session_id=session_id, - user_id=user_id, - temperature=temperature, - client=client, - agent_instances=agent_instances, # Pass agent instances to the planner - ) - agents[group_chat_manager_type] = group_chat_manager - - return agents - - @classmethod - def get_agent_class(cls, agent_type: AgentType) -> Type[BaseAgent]: - """Get the agent class for the specified type. - - Args: - agent_type: The agent type - - Returns: - The agent class - - Raises: - ValueError: If the agent type is unknown - """ - agent_class = cls._agent_classes.get(agent_type) - if not agent_class: - raise ValueError(f"Unknown agent type: {agent_type}") - return agent_class - - @classmethod - def clear_cache(cls, session_id: Optional[str] = None) -> None: - """Clear the agent cache. - - Args: - session_id: If provided, clear only this session's cache - """ - if session_id: - if session_id in cls._agent_cache: - del cls._agent_cache[session_id] - logger.info(f"Cleared agent cache for session {session_id}") - if session_id in cls._azure_ai_agent_cache: - del cls._azure_ai_agent_cache[session_id] - logger.info(f"Cleared Azure AI agent cache for session {session_id}") - else: - cls._agent_cache.clear() - cls._azure_ai_agent_cache.clear() - logger.info("Cleared all agent caches") diff --git a/src/backend/kernel_agents/agent_utils.py b/src/backend/kernel_agents/agent_utils.py deleted file mode 100644 index 2310d50cf..000000000 --- a/src/backend/kernel_agents/agent_utils.py +++ /dev/null @@ -1,89 +0,0 @@ -import json -from typing import Optional - -import semantic_kernel as sk -from pydantic import BaseModel - -from common.models.messages_kernel import Step -from common.database.database_factory import DatabaseFactory - -common_agent_system_message = "If you do not have the information for the arguments of the function you need to call, do not call the function. Instead, respond back to the user requesting further information. You must not hallucinate or invent any of the information used as arguments in the function. For example, if you need to call a function that requires a delivery address, you must not generate 123 Example St. You must skip calling functions and return a clarification message along the lines of: Sorry, I'm missing some information I need to help you with that. Could you please provide the delivery address so I can do that for you?" - - -class FSMStateAndTransition(BaseModel): - """Model for state and transition in a finite state machine.""" - - identifiedTargetState: str - identifiedTargetTransition: str - - -async def extract_and_update_transition_states( - step: Step, - session_id: str, - user_id: str, - planner_dynamic_or_workflow: str, - kernel: sk.Kernel, -) -> Optional[Step]: - """ - This function extracts the identified target state and transition from the LLM response and updates - the step with the identified target state and transition. This is reliant on the agent_reply already being present. - - Args: - step: The step to update - session_id: The current session ID - user_id: The user ID - planner_dynamic_or_workflow: Type of planner - kernel: The semantic kernel instance - - Returns: - The updated step or None if extraction fails - """ - planner_dynamic_or_workflow = "workflow" - if planner_dynamic_or_workflow == "workflow": - cosmos = await DatabaseFactory.get_database() - - # Create chat history for the semantic kernel completion - messages = [ - {"role": "assistant", "content": step.action}, - {"role": "assistant", "content": step.agent_reply}, - { - "role": "assistant", - "content": "Based on the above conversation between two agents, I need you to identify the identifiedTargetState and identifiedTargetTransition values. Only return these values. Do not make any function calls. If you are unable to work out the next transition state, return ERROR.", - }, - ] - - # Get the LLM response using semantic kernel - completion_service = kernel.get_service("completion") - - try: - completion_result = await completion_service.complete_chat_async( - messages=messages, - execution_settings={"response_format": {"type": "json_object"}}, - ) - - content = completion_result - - # Parse the LLM response - parsed_result = json.loads(content) - structured_plan = FSMStateAndTransition(**parsed_result) - - # Update the step - step.identified_target_state = structured_plan.identifiedTargetState - step.identified_target_transition = ( - structured_plan.identifiedTargetTransition - ) - - await cosmos.update_step(step) - return step - - except Exception as e: - print(f"Error extracting transition states: {e}") - return None - - -# The commented-out functions below would be implemented when needed -# async def set_next_viable_step_to_runnable(session_id): -# pass - -# async def initiate_replanning(session_id): -# pass diff --git a/src/backend/kernel_agents/generic_agent.py b/src/backend/kernel_agents/generic_agent.py deleted file mode 100644 index c68bdb280..000000000 --- a/src/backend/kernel_agents/generic_agent.py +++ /dev/null @@ -1,139 +0,0 @@ -import logging -from typing import Dict, List, Optional - -from kernel_agents.agent_base import BaseAgent -from kernel_tools.generic_tools import GenericTools -from common.models.messages_kernel import AgentType -from semantic_kernel.functions import KernelFunction - -from common.database.database_base import DatabaseBase - - -class GenericAgent(BaseAgent): - """Generic agent implementation using Semantic Kernel.""" - - def __init__( - self, - session_id: str, - user_id: str, - memory_store: Optional[DatabaseBase] = None, - tools: Optional[List[KernelFunction]] = None, - system_message: Optional[str] = None, - agent_name: str = AgentType.GENERIC.value, - client=None, - definition=None, - ) -> None: - """Initialize the Generic Agent. - - Args: - session_id: The current session identifier - user_id: The user identifier - memory_store: The Cosmos memory context - tools: List of tools available to this agent (optional) - system_message: Optional system message for the agent - agent_name: Optional name for the agent (defaults to "GenericAgent") - config_path: Optional path to the Generic tools configuration file - client: Optional client instance - definition: Optional definition instance - """ - # Load configuration if tools not provided - if not tools: - # Get tools directly from GenericTools class - tools_dict = GenericTools.get_all_kernel_functions() - - tools = [KernelFunction.from_method(func) for func in tools_dict.values()] - - # Use system message from config if not explicitly provided - if not system_message: - system_message = self.default_system_message(agent_name) - - # Use agent name from config if available - agent_name = AgentType.GENERIC.value - - # Call the parent initializer - super().__init__( - agent_name=agent_name, - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - client=client, - definition=definition, - ) - - @classmethod - async def create( - cls, - **kwargs: Dict[str, str], - ) -> None: - """Asynchronously create the PlannerAgent. - - Creates the Azure AI Agent for planning operations. - - Returns: - None - """ - - session_id = kwargs.get("session_id") - user_id = kwargs.get("user_id") - memory_store = kwargs.get("memory_store") - tools = kwargs.get("tools", None) - system_message = kwargs.get("system_message", None) - agent_name = kwargs.get("agent_name") - client = kwargs.get("client") - - try: - logging.info("Initializing GenericAgent from async init azure AI Agent") - - # Create the Azure AI Agent using AppConfig with string instructions - agent_definition = await cls._create_azure_ai_agent_definition( - agent_name=agent_name, - instructions=system_message, # Pass the formatted string, not an object - temperature=0.0, - response_format=None, - ) - - return cls( - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - agent_name=agent_name, - client=client, - definition=agent_definition, - ) - - except Exception as e: - logging.error(f"Failed to create Azure AI Agent for PlannerAgent: {e}") - raise - - @staticmethod - def default_system_message(agent_name=None) -> str: - """Get the default system message for the agent. - Args: - agent_name: The name of the agent (optional) - Returns: - The default system message for the agent - """ - return "You are a Generic agent that can help with general questions and provide basic information. You can search for information and perform simple calculations." - - @property - def plugins(self): - """Get the plugins for the generic agent.""" - return GenericTools.get_all_kernel_functions() - - # Explicitly inherit handle_action_request from the parent class - async def handle_action_request(self, action_request_json: str) -> str: - """Handle an action request from another agent or the system. - - This method is inherited from BaseAgent but explicitly included here for clarity. - - Args: - action_request_json: The action request as a JSON string - - Returns: - A JSON string containing the action response - """ - return await super().handle_action_request(action_request_json) diff --git a/src/backend/kernel_agents/group_chat_manager.py b/src/backend/kernel_agents/group_chat_manager.py deleted file mode 100644 index 588295c1a..000000000 --- a/src/backend/kernel_agents/group_chat_manager.py +++ /dev/null @@ -1,448 +0,0 @@ -import logging -from datetime import datetime -from typing import Dict, List, Optional - -from common.utils.event_utils import track_event_if_configured -from kernel_agents.agent_base import BaseAgent -from common.utils.utils_date import format_date_for_user -from common.models.messages_kernel import ( - ActionRequest, - AgentMessage, - AgentType, - HumanFeedback, - HumanFeedbackStatus, - InputTask, - Plan, - Step, - StepStatus, -) - -# pylint: disable=E0611 -from semantic_kernel.functions.kernel_function import KernelFunction - -from common.database.database_base import DatabaseBase - - -class GroupChatManager(BaseAgent): - """GroupChatManager agent implementation using Semantic Kernel. - - This agent creates and manages plans based on user tasks, breaking them down into steps - that can be executed by specialized agents to achieve the user's goal. - """ - - def __init__( - self, - session_id: str, - user_id: str, - memory_store: Optional[DatabaseBase] = None, - tools: Optional[List[KernelFunction]] = None, - system_message: Optional[str] = None, - agent_name: str = AgentType.GROUP_CHAT_MANAGER.value, - agent_tools_list: List[str] = None, - agent_instances: Optional[Dict[str, BaseAgent]] = None, - client=None, - definition=None, - ) -> None: - """Initialize the GroupChatManager Agent. - - Args: - session_id: The current session identifier - user_id: The user identifier - memory_store: The Cosmos memory context - system_message: Optional system message for the agent - agent_name: Optional name for the agent (defaults to "GroupChatManagerAgent") - config_path: Optional path to the configuration file - available_agents: List of available agent names for creating steps - agent_tools_list: List of available tools across all agents - agent_instances: Dictionary of agent instances available to the GroupChatManager - client: Optional client instance (passed to BaseAgent) - definition: Optional definition instance (passed to BaseAgent) - """ - # Default system message if not provided - if not system_message: - system_message = self.default_system_message(agent_name) - - # Initialize the base agent - super().__init__( - agent_name=agent_name, - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - client=client, - definition=definition, - ) - - # Store additional GroupChatManager-specific attributes - self._available_agents = [ - AgentType.HUMAN.value, - AgentType.HR.value, - AgentType.MARKETING.value, - AgentType.PRODUCT.value, - AgentType.PROCUREMENT.value, - AgentType.TECH_SUPPORT.value, - AgentType.GENERIC.value, - ] - self._agent_tools_list = agent_tools_list or [] - self._agent_instances = agent_instances or {} - - # Create the Azure AI Agent for group chat operations - # This will be initialized in async_init - self._azure_ai_agent = None - - @classmethod - async def create( - cls, - **kwargs: Dict[str, str], - ) -> None: - """Asynchronously create the PlannerAgent. - - Creates the Azure AI Agent for planning operations. - - Returns: - None - """ - - session_id = kwargs.get("session_id") - user_id = kwargs.get("user_id") - memory_store = kwargs.get("memory_store") - tools = kwargs.get("tools", None) - system_message = kwargs.get("system_message", None) - agent_name = kwargs.get("agent_name") - agent_tools_list = kwargs.get("agent_tools_list", None) - agent_instances = kwargs.get("agent_instances", None) - client = kwargs.get("client") - - try: - logging.info("Initializing GroupChatAgent from async init azure AI Agent") - - # Create the Azure AI Agent using AppConfig with string instructions - agent_definition = await cls._create_azure_ai_agent_definition( - agent_name=agent_name, - instructions=system_message, # Pass the formatted string, not an object - temperature=0.0, - response_format=None, - ) - - return cls( - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - agent_name=agent_name, - agent_tools_list=agent_tools_list, - agent_instances=agent_instances, - client=client, - definition=agent_definition, - ) - - except Exception as e: - logging.error(f"Failed to create Azure AI Agent for PlannerAgent: {e}") - raise - - @staticmethod - def default_system_message(agent_name=None) -> str: - """Get the default system message for the agent. - Args: - agent_name: The name of the agent (optional) - Returns: - The default system message for the agent - """ - return "You are a GroupChatManager agent responsible for creating and managing plans. You analyze tasks, break them down into steps, and assign them to the appropriate specialized agents." - - async def handle_input_task(self, message: InputTask) -> Plan: - """ - Handles the input task from the user. This is the initial message that starts the conversation. - This method should create a new plan. - """ - logging.info(f"Received input task: {message}") - await self._memory_store.add_item( - AgentMessage( - session_id=message.session_id, - user_id=self._user_id, - plan_id="", - content=f"{message.description}", - source=AgentType.HUMAN.value, - step_id="", - ) - ) - - track_event_if_configured( - "Group Chat Manager - Received and added input task into the cosmos", - { - "session_id": message.session_id, - "user_id": self._user_id, - "content": message.description, - "source": AgentType.HUMAN.value, - }, - ) - - # Send the InputTask to the PlannerAgent - planner_agent = self._agent_instances[AgentType.PLANNER.value] - result = await planner_agent.handle_input_task(message) - logging.info(f"Plan created: {result}") - return result - - async def handle_human_feedback(self, message: HumanFeedback) -> None: - """ - Handles the human approval feedback for a single step or all steps. - Updates the step status and stores the feedback in the session context. - - class HumanFeedback(BaseModel): - step_id: str - plan_id: str - session_id: str - approved: bool - human_feedback: Optional[str] = None - updated_action: Optional[str] = None - - class Step(BaseDataModel): - - data_type: Literal["step"] = Field("step", Literal=True) - plan_id: str - action: str - agent: BAgentType - status: StepStatus = StepStatus.planned - agent_reply: Optional[str] = None - human_feedback: Optional[str] = None - human_approval_status: Optional[HumanFeedbackStatus] = HumanFeedbackStatus.requested - updated_action: Optional[str] = None - session_id: ( - str # Added session_id to the Step model to partition the steps by session_id - ) - ts: Optional[int] = None - """ - # Need to retrieve all the steps for the plan - logging.info(f"GroupChatManager Received human feedback: {message}") - - steps: List[Step] = await self._memory_store.get_steps_by_plan(message.plan_id) - # Filter for steps that are planned or awaiting feedback - - # Get the first step assigned to HumanAgent for feedback - human_feedback_step: Step = next( - (s for s in steps if s.agent == AgentType.HUMAN), None - ) - - # Determine the feedback to use - if human_feedback_step and human_feedback_step.human_feedback: - # Use the provided human feedback if available - received_human_feedback_on_step = human_feedback_step.human_feedback - else: - received_human_feedback_on_step = "" - - # Provide generic context to the model - current_date = datetime.now().strftime("%Y-%m-%d") - formatted_date = format_date_for_user(current_date) - general_information = f"Today's date is {formatted_date}." - - # Get the general background information provided by the user in regards to the overall plan (not the steps) to add as context. - plan = await self._memory_store.get_plan_by_session( - session_id=message.session_id - ) - if plan.human_clarification_response: - received_human_feedback_on_plan = ( - f"{plan.human_clarification_request}: {plan.human_clarification_response}" - + " This information may or may not be relevant to the step you are executing - it was feedback provided by the human user on the overall plan, which includes multiple steps, not just the one you are actioning now." - ) - else: - received_human_feedback_on_plan = ( - "No human feedback provided on the overall plan." - ) - # Combine all feedback into a single string - received_human_feedback = ( - f"{received_human_feedback_on_step} " - f"{general_information} " - f"{received_human_feedback_on_plan}" - ) - - # Update and execute the specific step if step_id is provided - if message.step_id: - step = next((s for s in steps if s.id == message.step_id), None) - if step: - await self._update_step_status( - step, message.approved, received_human_feedback - ) - if message.approved: - await self._execute_step(message.session_id, step) - else: - # Notify the GroupChatManager that the step has been rejected - # TODO: Implement this logic later - step.status = StepStatus.rejected - step.human_approval_status = HumanFeedbackStatus.rejected - self._memory_store.update_step(step) - track_event_if_configured( - "Group Chat Manager - Steps has been rejected and updated into the cosmos", - { - "status": StepStatus.rejected, - "session_id": message.session_id, - "user_id": self._user_id, - "human_approval_status": HumanFeedbackStatus.rejected, - "source": step.agent, - }, - ) - else: - # Update and execute all steps if no specific step_id is provided - for step in steps: - await self._update_step_status( - step, message.approved, received_human_feedback - ) - if message.approved: - await self._execute_step(message.session_id, step) - else: - # Notify the GroupChatManager that the step has been rejected - # TODO: Implement this logic later - step.status = StepStatus.rejected - step.human_approval_status = HumanFeedbackStatus.rejected - self._memory_store.update_step(step) - track_event_if_configured( - f"{AgentType.GROUP_CHAT_MANAGER.value} - Step has been rejected and updated into the cosmos", - { - "status": StepStatus.rejected, - "session_id": message.session_id, - "user_id": self._user_id, - "human_approval_status": HumanFeedbackStatus.rejected, - "source": step.agent, - }, - ) - - # Function to update step status and add feedback - async def _update_step_status( - self, step: Step, approved: bool, received_human_feedback: str - ): - if approved: - step.status = StepStatus.approved - step.human_approval_status = HumanFeedbackStatus.accepted - else: - step.status = StepStatus.rejected - step.human_approval_status = HumanFeedbackStatus.rejected - - step.human_feedback = received_human_feedback - step.status = StepStatus.completed - await self._memory_store.update_step(step) - track_event_if_configured( - f"{AgentType.GROUP_CHAT_MANAGER.value} - Received human feedback, Updating step and updated into the cosmos", - { - "status": StepStatus.completed, - "session_id": step.session_id, - "user_id": self._user_id, - "human_feedback": received_human_feedback, - "source": step.agent, - }, - ) - - async def _execute_step(self, session_id: str, step: Step): - """ - Executes the given step by sending an ActionRequest to the appropriate agent. - """ - # Update step status to 'action_requested' - step.status = StepStatus.action_requested - await self._memory_store.update_step(step) - track_event_if_configured( - f"{AgentType.GROUP_CHAT_MANAGER.value} - Update step to action_requested and updated into the cosmos", - { - "status": StepStatus.action_requested, - "session_id": step.session_id, - "user_id": self._user_id, - "source": step.agent, - }, - ) - - # generate conversation history for the invoked agent - plan = await self._memory_store.get_plan_by_session(session_id=session_id) - steps: List[Step] = await self._memory_store.get_steps_by_plan(plan.id) - - current_step_id = step.id - # Initialize the formatted string - formatted_string = "" - formatted_string += "Here is the conversation history so far for the current plan. This information may or may not be relevant to the step you have been asked to execute." - formatted_string += f"The user's task was:\n{plan.summary}\n\n" - formatted_string += ( - f" human_clarification_request:\n{plan.human_clarification_request}\n\n" - ) - formatted_string += ( - f" human_clarification_response:\n{plan.human_clarification_response}\n\n" - ) - formatted_string += ( - "The conversation between the previous agents so far is below:\n" - ) - - # Iterate over the steps until the current_step_id - for i, step in enumerate(steps): - if step.id == current_step_id: - break - formatted_string += f"Step {i}\n" - formatted_string += f"{AgentType.GROUP_CHAT_MANAGER.value}: {step.action}\n" - formatted_string += f"{step.agent.value}: {step.agent_reply}\n" - formatted_string += "" - - logging.info(f"Formatted string: {formatted_string}") - - action_with_history = f"{formatted_string}. Here is the step to action: {step.action}. ONLY perform the steps and actions required to complete this specific step, the other steps have already been completed. Only use the conversational history for additional information, if it's required to complete the step you have been assigned." - - # Send action request to the appropriate agent - action_request = ActionRequest( - step_id=step.id, - plan_id=step.plan_id, - session_id=session_id, - action=action_with_history, - agent=step.agent, - ) - logging.info(f"Sending ActionRequest to {step.agent.value}") - - if step.agent != "": - agent_name = step.agent.value - formatted_agent = agent_name.replace("_", " ") - else: - raise ValueError(f"Check {step.agent} is missing") - - await self._memory_store.add_item( - AgentMessage( - session_id=session_id, - user_id=self._user_id, - plan_id=step.plan_id, - content=f"Requesting {formatted_agent} to perform action: {step.action}", - source=AgentType.GROUP_CHAT_MANAGER.value, - step_id=step.id, - ) - ) - - track_event_if_configured( - f"{AgentType.GROUP_CHAT_MANAGER.value} - Requesting {formatted_agent} to perform the action and added into the cosmos", - { - "session_id": session_id, - "user_id": self._user_id, - "plan_id": step.plan_id, - "content": f"Requesting {formatted_agent} to perform action: {step.action}", - "source": AgentType.GROUP_CHAT_MANAGER.value, - "step_id": step.id, - }, - ) - - if step.agent == AgentType.HUMAN.value: - # we mark the step as complete since we have received the human feedback - # Update step status to 'completed' - step.status = StepStatus.completed - await self._memory_store.update_step(step) - logging.info( - "Marking the step as complete - Since we have received the human feedback" - ) - track_event_if_configured( - "Group Chat Manager - Steps completed - Received the human feedback and updated into the cosmos", - { - "session_id": session_id, - "user_id": self._user_id, - "plan_id": step.plan_id, - "content": "Marking the step as complete - Since we have received the human feedback", - "source": step.agent, - "step_id": step.id, - }, - ) - else: - # Use the agent from the step to determine which agent to send to - agent = self._agent_instances[step.agent.value] - await agent.handle_action_request( - action_request - ) # this function is in base_agent.py - logging.info(f"Sent ActionRequest to {step.agent.value}") diff --git a/src/backend/kernel_agents/hr_agent.py b/src/backend/kernel_agents/hr_agent.py deleted file mode 100644 index ee1061f13..000000000 --- a/src/backend/kernel_agents/hr_agent.py +++ /dev/null @@ -1,127 +0,0 @@ -import logging -from typing import Dict, List, Optional - -from kernel_agents.agent_base import BaseAgent -from kernel_tools.hr_tools import HrTools -from common.models.messages_kernel import AgentType -from semantic_kernel.functions import KernelFunction - -from common.database.database_base import DatabaseBase - - -class HrAgent(BaseAgent): - """HR agent implementation using Semantic Kernel. - - This agent provides HR-related functions such as onboarding, benefits management, - and employee administration. - """ - - def __init__( - self, - session_id: str, - user_id: str, - memory_store: Optional[DatabaseBase] = None, - tools: Optional[List[KernelFunction]] = None, - system_message: Optional[str] = None, - agent_name: str = AgentType.HR.value, - client=None, - definition=None, - ) -> None: - """Initialize the HR Agent. - - Args: - kernel: The semantic kernel instance - session_id: The current session identifier - user_id: The user identifier - memory_store: The Cosmos memory context - tools: List of tools available to this agent (optional) - system_message: Optional system message for the agent - agent_name: Optional name for the agent (defaults to "HrAgent") - config_path: Optional path to the HR tools configuration file - client: Optional client instance - definition: Optional definition instance - """ - # Load configuration if tools not provided - if not tools: - # Get tools directly from HrTools class - tools_dict = HrTools.get_all_kernel_functions() - tools = [KernelFunction.from_method(func) for func in tools_dict.values()] - - # Use system message from config if not explicitly provided - if not system_message: - system_message = self.default_system_message(agent_name) - # Use agent name from config if available - agent_name = AgentType.HR.value - - super().__init__( - agent_name=agent_name, - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - client=client, - definition=definition, - ) - - @classmethod - async def create( - cls, - **kwargs: Dict[str, str], - ) -> None: - """Asynchronously create the PlannerAgent. - - Creates the Azure AI Agent for planning operations. - - Returns: - None - """ - - session_id = kwargs.get("session_id") - user_id = kwargs.get("user_id") - memory_store = kwargs.get("memory_store") - tools = kwargs.get("tools", None) - system_message = kwargs.get("system_message", None) - agent_name = kwargs.get("agent_name") - client = kwargs.get("client") - - try: - logging.info("Initializing HRAgent from async init azure AI Agent") - - # Create the Azure AI Agent using AppConfig with string instructions - agent_definition = await cls._create_azure_ai_agent_definition( - agent_name=agent_name, - instructions=system_message, # Pass the formatted string, not an object - temperature=0.0, - response_format=None, - ) - - return cls( - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - agent_name=agent_name, - client=client, - definition=agent_definition, - ) - - except Exception as e: - logging.error(f"Failed to create Azure AI Agent for PlannerAgent: {e}") - raise - - @staticmethod - def default_system_message(agent_name=None) -> str: - """Get the default system message for the agent. - Args: - agent_name: The name of the agent (optional) - Returns: - The default system message for the agent - """ - return "You are an AI Agent. You have knowledge about HR (e.g., human resources), policies, procedures, and onboarding guidelines." - - @property - def plugins(self): - """Get the plugins for the HR agent.""" - return HrTools.get_all_kernel_functions() diff --git a/src/backend/kernel_agents/human_agent.py b/src/backend/kernel_agents/human_agent.py deleted file mode 100644 index 3dc1a4f6b..000000000 --- a/src/backend/kernel_agents/human_agent.py +++ /dev/null @@ -1,269 +0,0 @@ -import logging -from typing import Dict, List, Optional - -from common.utils.event_utils import track_event_if_configured -from kernel_agents.agent_base import BaseAgent -from common.models.messages_kernel import ( - AgentMessage, - AgentType, - ApprovalRequest, - HumanClarification, - HumanFeedback, - StepStatus, -) -from semantic_kernel.functions import KernelFunction - -from common.database.database_base import DatabaseBase - - -class HumanAgent(BaseAgent): - """Human agent implementation using Semantic Kernel. - - This agent specializes in representing and assisting humans in the multi-agent system. - """ - - def __init__( - self, - session_id: str, - user_id: str, - memory_store: Optional[DatabaseBase] = None, - tools: Optional[List[KernelFunction]] = None, - system_message: Optional[str] = None, - agent_name: str = AgentType.HUMAN.value, - client=None, - definition=None, - ) -> None: - """Initialize the Human Agent. - - Args: - session_id: The current session identifier - user_id: The user identifier - memory_store: The Cosmos memory context - tools: List of tools available to this agent (optional) - system_message: Optional system message for the agent - agent_name: Optional name for the agent (defaults to "HumanAgent") - config_path: Optional path to the Human tools configuration file - client: Optional client instance - definition: Optional definition instance - """ - - # Use system message from config if not explicitly provided - if not system_message: - system_message = self.default_system_message(agent_name) - - # Use agent name from config if available - agent_name = AgentType.HUMAN.value - - super().__init__( - agent_name=agent_name, - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - client=client, - definition=definition, - ) - - @classmethod - async def create( - cls, - **kwargs: Dict[str, str], - ) -> None: - """Asynchronously create the PlannerAgent. - - Creates the Azure AI Agent for planning operations. - - Returns: - None - """ - - session_id = kwargs.get("session_id") - user_id = kwargs.get("user_id") - memory_store = kwargs.get("memory_store") - tools = kwargs.get("tools", None) - system_message = kwargs.get("system_message", None) - agent_name = kwargs.get("agent_name") - client = kwargs.get("client") - - try: - logging.info("Initializing HumanAgent from async init azure AI Agent") - - # Create the Azure AI Agent using AppConfig with string instructions - agent_definition = await cls._create_azure_ai_agent_definition( - agent_name=agent_name, - instructions=system_message, # Pass the formatted string, not an object - temperature=0.0, - response_format=None, - ) - - return cls( - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - agent_name=agent_name, - client=client, - definition=agent_definition, - ) - - except Exception as e: - logging.error(f"Failed to create Azure AI Agent for PlannerAgent: {e}") - raise - - @staticmethod - def default_system_message(agent_name=None) -> str: - """Get the default system message for the agent. - Args: - agent_name: The name of the agent (optional) - Returns: - The default system message for the agent - """ - return "You are representing a human user in the conversation. You handle interactions that require human feedback or input, such as providing clarification, approving plans, or giving feedback on steps." - - async def handle_human_feedback(self, human_feedback: HumanFeedback) -> str: - """Handle human feedback on a step. - - This method processes feedback provided by a human user on a specific step in a plan. - It updates the step with the feedback, marks the step as completed, and notifies the - GroupChatManager by creating an ApprovalRequest in the memory store. - - Args: - human_feedback: The HumanFeedback object containing feedback details - including step_id, session_id, and human_feedback text - - Returns: - Status message indicating success or failure of processing the feedback - """ - - # Get the step - step = await self._memory_store.get_step( - human_feedback.step_id, human_feedback.session_id - ) - if not step: - return f"Step {human_feedback.step_id} not found" - - # Update the step with the feedback - step.human_feedback = human_feedback.human_feedback - step.status = StepStatus.completed - - # Save the updated step - await self._memory_store.update_step(step) - await self._memory_store.add_item( - AgentMessage( - session_id=human_feedback.session_id, - user_id=step.user_id, - plan_id=step.plan_id, - content=f"Received feedback for step: {step.action}", - source=AgentType.HUMAN.value, - step_id=human_feedback.step_id, - ) - ) - - # Track the event - track_event_if_configured( - "Human Agent - Received feedback for step and added into the cosmos", - { - "session_id": human_feedback.session_id, - "user_id": self._user_id, - "plan_id": step.plan_id, - "content": f"Received feedback for step: {step.action}", - "source": AgentType.HUMAN.value, - "step_id": human_feedback.step_id, - }, - ) - - # Notify the GroupChatManager that the step has been completed - await self._memory_store.add_item( - ApprovalRequest( - session_id=human_feedback.session_id, - user_id=self._user_id, - plan_id=step.plan_id, - step_id=human_feedback.step_id, - agent_id=AgentType.GROUP_CHAT_MANAGER.value, - ) - ) - - # Track the approval request event - track_event_if_configured( - "Human Agent - Approval request sent for step and added into the cosmos", - { - "session_id": human_feedback.session_id, - "user_id": self._user_id, - "plan_id": step.plan_id, - "step_id": human_feedback.step_id, - "agent_id": "GroupChatManager", - }, - ) - - return "Human feedback processed successfully" - - async def handle_human_clarification( - self, human_clarification: HumanClarification - ) -> str: - """Provide clarification on a plan. - - This method stores human clarification information for a plan associated with a session. - It retrieves the plan from memory, updates it with the clarification text, and records - the event in telemetry. - - Args: - human_clarification: The HumanClarification object containing the session_id - and human_clarification provided by the human user - - Returns: - Status message indicating success or failure of adding the clarification - """ - session_id = human_clarification.session_id - clarification_text = human_clarification.human_clarification - - # Get the plan associated with this session - plan = await self._memory_store.get_plan_by_session(session_id) - if not plan: - return f"No plan found for session {session_id}" - - # Update the plan with the clarification - plan.human_clarification_response = clarification_text - await self._memory_store.update_plan(plan) - await self._memory_store.add_item( - AgentMessage( - session_id=session_id, - user_id=self._user_id, - plan_id="", - content=f"{clarification_text}", - source=AgentType.HUMAN.value, - step_id="", - ) - ) - # Track the event - track_event_if_configured( - "Human Agent - Provided clarification for plan", - { - "session_id": session_id, - "user_id": self._user_id, - "plan_id": plan.id, - "clarification": clarification_text, - "source": AgentType.HUMAN.value, - }, - ) - await self._memory_store.add_item( - AgentMessage( - session_id=session_id, - user_id=self._user_id, - plan_id="", - content="Thanks. The plan has been updated.", - source=AgentType.PLANNER.value, - step_id="", - ) - ) - track_event_if_configured( - "Planner - Updated with HumanClarification and added into the cosmos", - { - "session_id": session_id, - "user_id": self._user_id, - "content": "Thanks. The plan has been updated.", - "source": AgentType.PLANNER.value, - }, - ) - return f"Clarification provided for plan {plan.id}" diff --git a/src/backend/kernel_agents/marketing_agent.py b/src/backend/kernel_agents/marketing_agent.py deleted file mode 100644 index 0f0a81d88..000000000 --- a/src/backend/kernel_agents/marketing_agent.py +++ /dev/null @@ -1,126 +0,0 @@ -import logging -from typing import Dict, List, Optional - -from kernel_agents.agent_base import BaseAgent -from kernel_tools.marketing_tools import MarketingTools -from common.models.messages_kernel import AgentType -from semantic_kernel.functions import KernelFunction - -from common.database.database_base import DatabaseBase - - -class MarketingAgent(BaseAgent): - """Marketing agent implementation using Semantic Kernel. - - This agent specializes in marketing, campaign management, and analyzing market data. - """ - - def __init__( - self, - session_id: str, - user_id: str, - memory_store: Optional[DatabaseBase] = None, - tools: Optional[List[KernelFunction]] = None, - system_message: Optional[str] = None, - agent_name: str = AgentType.MARKETING.value, - client=None, - definition=None, - ) -> None: - """Initialize the Marketing Agent. - - Args: - kernel: The semantic kernel instance - session_id: The current session identifier - user_id: The user identifier - memory_store: The Cosmos memory context - tools: List of tools available to this agent (optional) - system_message: Optional system message for the agent - agent_name: Optional name for the agent (defaults to "MarketingAgent") - client: Optional client instance - definition: Optional definition instance - """ - # Load configuration if tools not provided - if not tools: - # Get tools directly from MarketingTools class - tools_dict = MarketingTools.get_all_kernel_functions() - tools = [KernelFunction.from_method(func) for func in tools_dict.values()] - - # Use system message from config if not explicitly provided - if not system_message: - system_message = self.default_system_message(agent_name) - - # Use agent name from config if available - agent_name = AgentType.MARKETING.value - - super().__init__( - agent_name=agent_name, - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - client=client, - definition=definition, - ) - - @classmethod - async def create( - cls, - **kwargs: Dict[str, str], - ) -> None: - """Asynchronously create the PlannerAgent. - - Creates the Azure AI Agent for planning operations. - - Returns: - None - """ - - session_id = kwargs.get("session_id") - user_id = kwargs.get("user_id") - memory_store = kwargs.get("memory_store") - tools = kwargs.get("tools", None) - system_message = kwargs.get("system_message", None) - agent_name = kwargs.get("agent_name") - client = kwargs.get("client") - - try: - logging.info("Initializing MarketingAgent from async init azure AI Agent") - - # Create the Azure AI Agent using AppConfig with string instructions - agent_definition = await cls._create_azure_ai_agent_definition( - agent_name=agent_name, - instructions=system_message, # Pass the formatted string, not an object - temperature=0.0, - response_format=None, - ) - - return cls( - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - agent_name=agent_name, - client=client, - definition=agent_definition, - ) - - except Exception as e: - logging.error(f"Failed to create Azure AI Agent for PlannerAgent: {e}") - raise - - @staticmethod - def default_system_message(agent_name=None) -> str: - """Get the default system message for the agent. - Args: - agent_name: The name of the agent (optional) - Returns: - The default system message for the agent - """ - return "You are a Marketing agent. You specialize in marketing strategy, campaign development, content creation, and market analysis. You help create effective marketing campaigns, analyze market data, and develop promotional content for products and services." - - @property - def plugins(self): - """Get the plugins for the marketing agent.""" - return MarketingTools.get_all_kernel_functions() diff --git a/src/backend/kernel_agents/planner_agent.py b/src/backend/kernel_agents/planner_agent.py deleted file mode 100644 index 452756228..000000000 --- a/src/backend/kernel_agents/planner_agent.py +++ /dev/null @@ -1,608 +0,0 @@ -import datetime -import logging -import uuid -from typing import Any, Dict, List, Optional, Tuple - -from azure.ai.agents.models import ( - ResponseFormatJsonSchema, - ResponseFormatJsonSchemaType, -) -from common.utils.event_utils import track_event_if_configured -from kernel_agents.agent_base import BaseAgent -from kernel_tools.generic_tools import GenericTools -from kernel_tools.hr_tools import HrTools -from kernel_tools.marketing_tools import MarketingTools -from kernel_tools.procurement_tools import ProcurementTools -from kernel_tools.product_tools import ProductTools -from kernel_tools.tech_support_tools import TechSupportTools -from common.models.messages_kernel import ( - AgentMessage, - AgentType, - HumanFeedbackStatus, - InputTask, - Plan, - PlannerResponsePlan, - PlanStatus, - Step, - StepStatus, -) -from semantic_kernel.functions import KernelFunction -from semantic_kernel.functions.kernel_arguments import KernelArguments - -from common.database.database_base import DatabaseBase - - -class PlannerAgent(BaseAgent): - """Planner agent implementation using Semantic Kernel. - - This agent creates and manages plans based on user tasks, breaking them down into steps - that can be executed by specialized agents to achieve the user's goal. - """ - - def __init__( - self, - session_id: str, - user_id: str, - memory_store: Optional[DatabaseBase] = None, - tools: Optional[List[KernelFunction]] = None, - system_message: Optional[str] = None, - agent_name: str = AgentType.PLANNER.value, - available_agents: List[str] = None, - agent_instances: Optional[Dict[str, BaseAgent]] = None, - client=None, - definition=None, - ) -> None: - """Initialize the Planner Agent. - - Args: - session_id: The current session identifier - user_id: The user identifier - memory_store: The Cosmos memory context - tools: Optional list of tools for this agent - system_message: Optional system message for the agent - agent_name: Optional name for the agent (defaults to "PlannerAgent") - config_path: Optional path to the configuration file - available_agents: List of available agent names for creating steps - agent_tools_list: List of available tools across all agents - agent_instances: Dictionary of agent instances available to the planner - client: Optional client instance (passed to BaseAgent) - definition: Optional definition instance (passed to BaseAgent) - """ - # Default system message if not provided - if not system_message: - system_message = self.default_system_message(agent_name) - - # Initialize the base agent - super().__init__( - agent_name=agent_name, - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - client=client, - definition=definition, - ) - - # Store additional planner-specific attributes - self._available_agents = available_agents or [ - AgentType.HUMAN.value, - AgentType.HR.value, - AgentType.MARKETING.value, - AgentType.PRODUCT.value, - AgentType.PROCUREMENT.value, - AgentType.TECH_SUPPORT.value, - AgentType.GENERIC.value, - ] - self._agent_tools_list = { - AgentType.HR: HrTools.generate_tools_json_doc(), - AgentType.MARKETING: MarketingTools.generate_tools_json_doc(), - AgentType.PRODUCT: ProductTools.generate_tools_json_doc(), - AgentType.PROCUREMENT: ProcurementTools.generate_tools_json_doc(), - AgentType.TECH_SUPPORT: TechSupportTools.generate_tools_json_doc(), - AgentType.GENERIC: GenericTools.generate_tools_json_doc(), - } - - self._agent_instances = agent_instances or {} - - @staticmethod - def default_system_message(agent_name=None) -> str: - """Get the default system message for the agent. - Args: - agent_name: The name of the agent (optional) - Returns: - The default system message for the agent - """ - return "You are a Planner agent responsible for creating and managing plans. You analyze tasks, break them down into steps, and assign them to the appropriate specialized agents." - - @classmethod - async def create( - cls, - **kwargs: Dict[str, Any], - ) -> None: - """Asynchronously create the PlannerAgent. - - Creates the Azure AI Agent for planning operations. - - Returns: - None - """ - - session_id = kwargs.get("session_id") - user_id = kwargs.get("user_id") - memory_store = kwargs.get("memory_store") - tools = kwargs.get("tools", None) - system_message = kwargs.get("system_message", None) - agent_name = kwargs.get("agent_name") - available_agents = kwargs.get("available_agents", None) - agent_instances = kwargs.get("agent_instances", None) - client = kwargs.get("client") - - # Create the instruction template - - try: - logging.info("Initializing PlannerAgent from async init azure AI Agent") - - # Create the Azure AI Agent using AppConfig with string instructions - agent_definition = await cls._create_azure_ai_agent_definition( - agent_name=agent_name, - instructions=cls._get_template(), # Pass the formatted string, not an object - temperature=0.0, - response_format=ResponseFormatJsonSchemaType( - json_schema=ResponseFormatJsonSchema( - name=PlannerResponsePlan.__name__, - description=f"respond with {PlannerResponsePlan.__name__.lower()}", - schema=PlannerResponsePlan.model_json_schema(), - ) - ), - ) - - return cls( - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - agent_name=agent_name, - available_agents=available_agents, - agent_instances=agent_instances, - client=client, - definition=agent_definition, - ) - - except Exception as e: - logging.error(f"Failed to create Azure AI Agent for PlannerAgent: {e}") - raise - - async def handle_input_task(self, input_task: InputTask) -> str: - """Handle the initial input task from the user. - - Args: - kernel_arguments: Contains the input_task_json string - - Returns: - Status message - """ - # Parse the input task - logging.info("Handling input task") - - plan, steps = await self._create_structured_plan(input_task) - - logging.info(f"Plan created: {plan}") - logging.info(f"Steps created: {steps}") - - if steps: - # Add a message about the created plan - await self._memory_store.add_item( - AgentMessage( - session_id=input_task.session_id, - user_id=self._user_id, - plan_id=plan.id, - content=f"Generated a plan with {len(steps)} steps. Click the checkmark beside each step to complete it, click the x to reject this step.", - source=AgentType.PLANNER.value, - step_id="", - ) - ) - - track_event_if_configured( - f"Planner - Generated a plan with {len(steps)} steps and added plan into the cosmos", - { - "session_id": input_task.session_id, - "user_id": self._user_id, - "plan_id": plan.id, - "content": f"Generated a plan with {len(steps)} steps. Click the checkmark beside each step to complete it, click the x to reject this step.", - "source": AgentType.PLANNER.value, - }, - ) - - # If human clarification is needed, add a message requesting it - if ( - hasattr(plan, "human_clarification_request") - and plan.human_clarification_request - ): - await self._memory_store.add_item( - AgentMessage( - session_id=input_task.session_id, - user_id=self._user_id, - plan_id=plan.id, - content=f"I require additional information before we can proceed: {plan.human_clarification_request}", - source=AgentType.PLANNER.value, - step_id="", - ) - ) - - track_event_if_configured( - "Planner - Additional information requested and added into the cosmos", - { - "session_id": input_task.session_id, - "user_id": self._user_id, - "plan_id": plan.id, - "content": f"I require additional information before we can proceed: {plan.human_clarification_request}", - "source": AgentType.PLANNER.value, - }, - ) - - return f"Plan '{plan.id}' created successfully with {len(steps)} steps" - - async def handle_plan_clarification(self, kernel_arguments: KernelArguments) -> str: - """Handle human clarification for a plan. - - Args: - kernel_arguments: Contains session_id and human_clarification - - Returns: - Status message - """ - session_id = kernel_arguments["session_id"] - human_clarification = kernel_arguments["human_clarification"] - - # Retrieve and update the plan - plan = await self._memory_store.get_plan_by_session(session_id) - if not plan: - return f"No plan found for session {session_id}" - - plan.human_clarification_response = human_clarification - await self._memory_store.update_plan(plan) - - # Add a record of the clarification - await self._memory_store.add_item( - AgentMessage( - session_id=session_id, - user_id=self._user_id, - plan_id="", - content=f"{human_clarification}", - source=AgentType.HUMAN.value, - step_id="", - ) - ) - - track_event_if_configured( - "Planner - Store HumanAgent clarification and added into the cosmos", - { - "session_id": session_id, - "user_id": self._user_id, - "content": f"{human_clarification}", - "source": AgentType.HUMAN.value, - }, - ) - - # Add a confirmation message - await self._memory_store.add_item( - AgentMessage( - session_id=session_id, - user_id=self._user_id, - plan_id="", - content="Thanks. The plan has been updated.", - source=AgentType.PLANNER.value, - step_id="", - ) - ) - - track_event_if_configured( - "Planner - Updated with HumanClarification and added into the cosmos", - { - "session_id": session_id, - "user_id": self._user_id, - "content": "Thanks. The plan has been updated.", - "source": AgentType.PLANNER.value, - }, - ) - - return "Plan updated with human clarification" - - async def _create_structured_plan( - self, input_task: InputTask - ) -> Tuple[Plan, List[Step]]: - """Create a structured plan with steps based on the input task. - - Args: - input_task: The input task from the user - - Returns: - Tuple containing the created plan and list of steps - """ - try: - # Generate the instruction for the LLM - - # Get template variables as a dictionary - args = self._generate_args(input_task.description) - - # Create kernel arguments - make sure we explicitly emphasize the task - kernel_args = KernelArguments(**args) - - thread = None - # thread = self.client.agents.create_thread(thread_id=input_task.session_id) - async_generator = self.invoke( - arguments=kernel_args, - settings={ - "temperature": 0.0, # Keep temperature low for consistent planning - "max_tokens": 10096, # Ensure we have enough tokens for the full plan - }, - thread=thread, - ) - - # Call invoke with proper keyword arguments and JSON response schema - response_content = "" - - # Collect the response from the async generator - async for chunk in async_generator: - if chunk is not None: - response_content += str(chunk) - - logging.info(f"Response content length: {len(response_content)}") - - # Check if response is empty or whitespace - if not response_content or response_content.isspace(): - raise ValueError("Received empty response from Azure AI Agent") - - # Parse the JSON response directly to PlannerResponsePlan - parsed_result = None - - # Try various parsing approaches in sequence - try: - # 1. First attempt: Try to parse the raw response directly - parsed_result = PlannerResponsePlan.parse_raw(response_content) - if parsed_result is None: - # If all parsing attempts fail, create a fallback plan from the text content - logging.info( - "All parsing attempts failed, creating fallback plan from text content" - ) - raise ValueError("Failed to parse JSON response") - - except Exception as parsing_exception: - logging.exception(f"Error during parsing attempts: {parsing_exception}") - raise ValueError("Failed to parse JSON response") - - # At this point, we have a valid parsed_result - - # Extract plan details - initial_goal = parsed_result.initial_goal - steps_data = parsed_result.steps - summary = parsed_result.summary_plan_and_steps - human_clarification_request = parsed_result.human_clarification_request - - # Create the Plan instance - plan = Plan( - id=str(uuid.uuid4()), - session_id=input_task.session_id, - user_id=self._user_id, - initial_goal=initial_goal, - overall_status=PlanStatus.in_progress, - summary=summary, - human_clarification_request=human_clarification_request, - ) - - # Store the plan - await self._memory_store.add_plan(plan) - - # Create steps from the parsed data - steps = [] - for step_data in steps_data: - action = step_data.action - agent_name = step_data.agent - - # Validate agent name - if agent_name not in self._available_agents: - logging.warning( - f"Invalid agent name: {agent_name}, defaulting to {AgentType.GENERIC.value}" - ) - agent_name = AgentType.GENERIC.value - - # Create the step - step = Step( - id=str(uuid.uuid4()), - plan_id=plan.id, - session_id=input_task.session_id, - user_id=self._user_id, - action=action, - agent=agent_name, - status=StepStatus.planned, - human_approval_status=HumanFeedbackStatus.requested, - ) - - # Store the step - await self._memory_store.add_step(step) - steps.append(step) - - try: - track_event_if_configured( - "Planner - Added planned individual step into the cosmos", - { - "plan_id": plan.id, - "action": action, - "agent": agent_name, - "status": StepStatus.planned, - "session_id": input_task.session_id, - "user_id": self._user_id, - "human_approval_status": HumanFeedbackStatus.requested, - }, - ) - except Exception as event_error: - # Don't let event tracking errors break the main flow - logging.warning(f"Error in event tracking: {event_error}") - - return plan, steps - - except Exception as e: - error_message = str(e) - if "Rate limit is exceeded" in error_message: - logging.warning("Rate limit hit. Consider retrying after some delay.") - raise - else: - logging.exception(f"Error creating structured plan: {e}") - - # Create a fallback dummy plan when parsing fails - logging.info("Creating fallback dummy plan due to parsing error") - - # Create a dummy plan with the original task description - dummy_plan = Plan( - id=str(uuid.uuid4()), - session_id=input_task.session_id, - user_id=self._user_id, - initial_goal=input_task.description, - overall_status=PlanStatus.in_progress, - summary=f"Plan created for: {input_task.description}", - human_clarification_request=None, - timestamp=datetime.datetime.utcnow().isoformat(), - ) - - # Store the dummy plan - await self._memory_store.add_plan(dummy_plan) - - # Create a dummy step for analyzing the task - dummy_step = Step( - id=str(uuid.uuid4()), - plan_id=dummy_plan.id, - session_id=input_task.session_id, - user_id=self._user_id, - action="Analyze the task: " + input_task.description, - agent=AgentType.GENERIC.value, # Using the correct value from AgentType enum - status=StepStatus.planned, - human_approval_status=HumanFeedbackStatus.requested, - timestamp=datetime.datetime.utcnow().isoformat(), - ) - - # Store the dummy step - await self._memory_store.add_step(dummy_step) - - # Add a second step to request human clarification - clarification_step = Step( - id=str(uuid.uuid4()), - plan_id=dummy_plan.id, - session_id=input_task.session_id, - user_id=self._user_id, - action=f"Provide more details about: {input_task.description}", - agent=AgentType.HUMAN.value, - status=StepStatus.planned, - human_approval_status=HumanFeedbackStatus.requested, - timestamp=datetime.datetime.utcnow().isoformat(), - ) - - # Store the clarification step - await self._memory_store.add_step(clarification_step) - - # Log the event - try: - track_event_if_configured( - "Planner - Created fallback dummy plan due to parsing error", - { - "session_id": input_task.session_id, - "user_id": self._user_id, - "error": str(e), - "description": input_task.description, - "source": AgentType.PLANNER.value, - }, - ) - except Exception as event_error: - logging.warning( - f"Error in event tracking during fallback: {event_error}" - ) - - return dummy_plan, [dummy_step, clarification_step] - - def _generate_args(self, objective: str) -> any: - """Generate instruction for the LLM to create a plan. - - Args: - objective: The user's objective - - Returns: - Dictionary containing the variables to populate the template - """ - # Create a list of available agents - agents_str = ", ".join(self._available_agents) - - # Create list of available tools in JSON-like format - tools_list = [] - - for agent_name, tools in self._agent_tools_list.items(): - if agent_name in self._available_agents: - tools_list.append(tools) - - tools_str = tools_list - - # Return a dictionary with template variables - return { - "objective": objective, - "agents_str": agents_str, - "tools_str": tools_str, - } - - @staticmethod - def _get_template(): - """Generate the instruction template for the LLM.""" - # Build the instruction with proper format placeholders for .format() method - - instruction_template = """ - You are the Planner, an AI orchestrator that manages a group of AI agents to accomplish tasks. - - For the given objective, come up with a simple step-by-step plan. - This plan should involve individual tasks that, if executed correctly, will yield the correct answer. Do not add any superfluous steps. - The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps. - - These actions are passed to the specific agent. Make sure the action contains all the information required for the agent to execute the task. - - Your objective is: - {{$objective}} - - The agents you have access to are: - {{$agents_str}} - - These agents have access to the following functions: - {{$tools_str}} - - The first step of your plan should be to ask the user for any additional information required to progress the rest of steps planned. - - Only use the functions provided as part of your plan. If the task is not possible with the agents and tools provided, create a step with the agent of type Human and mark the overall status as completed. - - Do not add superfluous steps - only take the most direct path to the solution, with the minimum number of steps. Only do the minimum necessary to complete the goal. - - If there is a single function call that can directly solve the task, only generate a plan with a single step. For example, if someone asks to be granted access to a database, generate a plan with only one step involving the grant_database_access function, with no additional steps. - - When generating the action in the plan, frame the action as an instruction you are passing to the agent to execute. It should be a short, single sentence. Include the function to use. For example, "Set up an Office 365 Account for Jessica Smith. Function: set_up_office_365_account" - - Ensure the summary of the plan and the overall steps is less than 50 words. - - Identify any additional information that might be required to complete the task. Include this information in the plan in the human_clarification_request field of the plan. If it is not required, leave it as null. - - When identifying required information, consider what input a GenericAgent or fallback LLM model would need to perform the task correctly. This may include: - - Input data, text, or content to process - - A question to answer or topic to describe - - Any referenced material that is mentioned but not actually included (e.g., "the given text") - - A clear subject or target when the task instruction is too vague (e.g., "describe," "summarize," or "analyze" without specifying what to describe) - - If such required input is missing—even if not explicitly referenced—generate a concise clarification request in the human_clarification_request field. - - Do not include information that you are waiting for clarification on in the string of the action field, as this otherwise won't get updated. - - You must prioritise using the provided functions to accomplish each step. First evaluate each and every function the agents have access too. Only if you cannot find a function needed to complete the task, and you have reviewed each and every function, and determined why each are not suitable, there are two options you can take when generating the plan. - First evaluate whether the step could be handled by a typical large language model, without any specialised functions. For example, tasks such as "add 32 to 54", or "convert this SQL code to a python script", or "write a 200 word story about a fictional product strategy". - If a general Large Language Model CAN handle the step/required action, add a step to the plan with the action you believe would be needed. Assign these steps to the GenericAgent. For example, if the task is to convert the following SQL into python code (SELECT * FROM employees;), and there is no function to convert SQL to python, write a step with the action "convert the following SQL into python code (SELECT * FROM employees;)" and assign it to the GenericAgent. - Alternatively, if a general Large Language Model CAN NOT handle the step/required action, add a step to the plan with the action you believe would be needed and assign it to the HumanAgent. For example, if the task is to find the best way to get from A to B, and there is no function to calculate the best route, write a step with the action "Calculate the best route from A to B." and assign it to the HumanAgent. - - Limit the plan to 6 steps or less. - - Choose from {{$agents_str}} ONLY for planning your steps. - - """ - return instruction_template diff --git a/src/backend/kernel_agents/procurement_agent.py b/src/backend/kernel_agents/procurement_agent.py deleted file mode 100644 index ab3258203..000000000 --- a/src/backend/kernel_agents/procurement_agent.py +++ /dev/null @@ -1,125 +0,0 @@ -import logging -from typing import Dict, List, Optional - -from kernel_agents.agent_base import BaseAgent -from kernel_tools.procurement_tools import ProcurementTools -from common.models.messages_kernel import AgentType -from semantic_kernel.functions import KernelFunction -from common.database.database_base import DatabaseBase - - -class ProcurementAgent(BaseAgent): - """Procurement agent implementation using Semantic Kernel. - - This agent specializes in procurement, purchasing, vendor management, and inventory tasks. - """ - - def __init__( - self, - session_id: str, - user_id: str, - memory_store: Optional[DatabaseBase] = None, - tools: Optional[List[KernelFunction]] = None, - system_message: Optional[str] = None, - agent_name: str = AgentType.PROCUREMENT.value, - client=None, - definition=None, - ) -> None: - """Initialize the Procurement Agent. - - Args: - kernel: The semantic kernel instance - session_id: The current session identifier - user_id: The user identifier - memory_store: The Cosmos memory context - tools: List of tools available to this agent (optional) - system_message: Optional system message for the agent - agent_name: Optional name for the agent (defaults to "ProcurementAgent") - client: Optional client instance - definition: Optional definition instance - """ - # Load configuration if tools not provided - if not tools: - # Get tools directly from ProcurementTools class - tools_dict = ProcurementTools.get_all_kernel_functions() - tools = [KernelFunction.from_method(func) for func in tools_dict.values()] - - # Use system message from config if not explicitly provided - if not system_message: - system_message = self.default_system_message(agent_name) - - # Use agent name from config if available - agent_name = AgentType.PROCUREMENT.value - - super().__init__( - agent_name=agent_name, - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - client=client, - definition=definition, - ) - - @classmethod - async def create( - cls, - **kwargs: Dict[str, str], - ) -> None: - """Asynchronously create the PlannerAgent. - - Creates the Azure AI Agent for planning operations. - - Returns: - None - """ - - session_id = kwargs.get("session_id") - user_id = kwargs.get("user_id") - memory_store = kwargs.get("memory_store") - tools = kwargs.get("tools", None) - system_message = kwargs.get("system_message", None) - agent_name = kwargs.get("agent_name") - client = kwargs.get("client") - - try: - logging.info("Initializing ProcurementAgent from async init azure AI Agent") - - # Create the Azure AI Agent using AppConfig with string instructions - agent_definition = await cls._create_azure_ai_agent_definition( - agent_name=agent_name, - instructions=system_message, # Pass the formatted string, not an object - temperature=0.0, - response_format=None, - ) - - return cls( - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - agent_name=agent_name, - client=client, - definition=agent_definition, - ) - - except Exception as e: - logging.error(f"Failed to create Azure AI Agent for PlannerAgent: {e}") - raise - - @staticmethod - def default_system_message(agent_name=None) -> str: - """Get the default system message for the agent. - Args: - agent_name: The name of the agent (optional) - Returns: - The default system message for the agent - """ - return "You are a Procurement agent. You specialize in purchasing, vendor management, supply chain operations, and inventory control. You help with creating purchase orders, managing vendors, tracking orders, and ensuring efficient procurement processes." - - @property - def plugins(self): - """Get the plugins for the procurement agent.""" - return ProcurementTools.get_all_kernel_functions() diff --git a/src/backend/kernel_agents/product_agent.py b/src/backend/kernel_agents/product_agent.py deleted file mode 100644 index 2aa605e3d..000000000 --- a/src/backend/kernel_agents/product_agent.py +++ /dev/null @@ -1,145 +0,0 @@ -import logging -from typing import Dict, List, Optional - -from kernel_agents.agent_base import BaseAgent -from kernel_tools.product_tools import ProductTools -from common.models.messages_kernel import AgentType -from semantic_kernel.functions import KernelFunction - -from common.database.database_base import DatabaseBase - - -class ProductAgent(BaseAgent): - """Product agent implementation using Semantic Kernel. - - This agent specializes in product management, development, and related tasks. - It can provide information about products, manage inventory, handle product - launches, analyze sales data, and coordinate with other teams like marketing - and tech support. - """ - - def __init__( - self, - session_id: str, - user_id: str, - memory_store: Optional[DatabaseBase] = None, - tools: Optional[List[KernelFunction]] = None, - system_message: Optional[str] = None, - agent_name: str = AgentType.PRODUCT.value, - client=None, - definition=None, - ) -> None: - """Initialize the Product Agent. - - Args: - kernel: The semantic kernel instance - session_id: The current session identifier - user_id: The user identifier - memory_store: The Cosmos memory context - tools: List of tools available to this agent (optional) - system_message: Optional system message for the agent - agent_name: Optional name for the agent (defaults to "ProductAgent") - config_path: Optional path to the Product tools configuration file - client: Optional client instance - definition: Optional definition instance - """ - # Load configuration if tools not provided - if not tools: - # Get tools directly from ProductTools class - tools_dict = ProductTools.get_all_kernel_functions() - tools = [KernelFunction.from_method(func) for func in tools_dict.values()] - - # Use system message from config if not explicitly provided - if not system_message: - system_message = self.default_system_message(agent_name) - - # Use agent name from config if available - agent_name = AgentType.PRODUCT.value - - super().__init__( - agent_name=agent_name, - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - client=client, - definition=definition, - ) - - @classmethod - async def create( - cls, - **kwargs: Dict[str, str], - ) -> None: - """Asynchronously create the PlannerAgent. - - Creates the Azure AI Agent for planning operations. - - Returns: - None - """ - - session_id = kwargs.get("session_id") - user_id = kwargs.get("user_id") - memory_store = kwargs.get("memory_store") - tools = kwargs.get("tools", None) - system_message = kwargs.get("system_message", None) - agent_name = kwargs.get("agent_name") - client = kwargs.get("client") - - try: - logging.info("Initializing ProductAgent from async init azure AI Agent") - - # Create the Azure AI Agent using AppConfig with string instructions - agent_definition = await cls._create_azure_ai_agent_definition( - agent_name=agent_name, - instructions=system_message, # Pass the formatted string, not an object - temperature=0.0, - response_format=None, - ) - - return cls( - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - agent_name=agent_name, - client=client, - definition=agent_definition, - ) - - except Exception as e: - logging.error(f"Failed to create Azure AI Agent for PlannerAgent: {e}") - raise - - @staticmethod - def default_system_message(agent_name=None) -> str: - """Get the default system message for the agent. - Args: - agent_name: The name of the agent (optional) - Returns: - The default system message for the agent - """ - return "You are a Product agent. You have knowledge about product management, development, and compliance guidelines. When asked to call a function, you should summarize back what was done." - - @property - def plugins(self): - """Get the plugins for the product agent.""" - return ProductTools.get_all_kernel_functions() - - # Explicitly inherit handle_action_request from the parent class - # This is not technically necessary but makes the inheritance explicit - async def handle_action_request(self, action_request_json: str) -> str: - """Handle an action request from another agent or the system. - - This method is inherited from BaseAgent but explicitly included here for clarity. - - Args: - action_request_json: The action request as a JSON string - - Returns: - A JSON string containing the action response - """ - return await super().handle_action_request(action_request_json) diff --git a/src/backend/kernel_agents/tech_support_agent.py b/src/backend/kernel_agents/tech_support_agent.py deleted file mode 100644 index 541ff529f..000000000 --- a/src/backend/kernel_agents/tech_support_agent.py +++ /dev/null @@ -1,126 +0,0 @@ -import logging -from typing import Dict, List, Optional - -from kernel_agents.agent_base import BaseAgent -from kernel_tools.tech_support_tools import TechSupportTools -from common.models.messages_kernel import AgentType -from semantic_kernel.functions import KernelFunction -from common.database.database_base import DatabaseBase - - -class TechSupportAgent(BaseAgent): - """Tech Support agent implementation using Semantic Kernel. - - This agent specializes in technical support, IT administration, and equipment setup. - """ - - def __init__( - self, - session_id: str, - user_id: str, - memory_store: Optional[DatabaseBase] = None, - tools: Optional[List[KernelFunction]] = None, - system_message: Optional[str] = None, - agent_name: str = AgentType.TECH_SUPPORT.value, - client=None, - definition=None, - ) -> None: - """Initialize the Tech Support Agent. - - Args: - kernel: The semantic kernel instance - session_id: The current session identifier - user_id: The user identifier - memory_store: The Cosmos memory context - tools: List of tools available to this agent (optional) - system_message: Optional system message for the agent - agent_name: Optional name for the agent (defaults to "TechSupportAgent") - config_path: Optional path to the Tech Support tools configuration file - client: Optional client instance - definition: Optional definition instance - """ - # Load configuration if tools not provided - if not tools: - # Get tools directly from TechSupportTools class - tools_dict = TechSupportTools.get_all_kernel_functions() - tools = [KernelFunction.from_method(func) for func in tools_dict.values()] - - # Use system message from config if not explicitly provided - if not system_message: - system_message = self.default_system_message(agent_name) - - # Use agent name from config if available - agent_name = AgentType.TECH_SUPPORT.value - - super().__init__( - agent_name=agent_name, - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - client=client, - definition=definition, - ) - - @classmethod - async def create( - cls, - **kwargs: Dict[str, str], - ) -> None: - """Asynchronously create the PlannerAgent. - - Creates the Azure AI Agent for planning operations. - - Returns: - None - """ - - session_id = kwargs.get("session_id") - user_id = kwargs.get("user_id") - memory_store = kwargs.get("memory_store") - tools = kwargs.get("tools", None) - system_message = kwargs.get("system_message", None) - agent_name = kwargs.get("agent_name") - client = kwargs.get("client") - - try: - logging.info("Initializing TechSupportAgent from async init azure AI Agent") - - # Create the Azure AI Agent using AppConfig with string instructions - agent_definition = await cls._create_azure_ai_agent_definition( - agent_name=agent_name, - instructions=system_message, # Pass the formatted string, not an object - temperature=0.0, - response_format=None, - ) - - return cls( - session_id=session_id, - user_id=user_id, - memory_store=memory_store, - tools=tools, - system_message=system_message, - agent_name=agent_name, - client=client, - definition=agent_definition, - ) - - except Exception as e: - logging.error(f"Failed to create Azure AI Agent for PlannerAgent: {e}") - raise - - @staticmethod - def default_system_message(agent_name=None) -> str: - """Get the default system message for the agent. - Args: - agent_name: The name of the agent (optional) - Returns: - The default system message for the agent - """ - return "You are a Product agent. You have knowledge about product management, development, and compliance guidelines. When asked to call a function, you should summarize back what was done." - - @property - def plugins(self): - """Get the plugins for the tech support agent.""" - return TechSupportTools.get_all_kernel_functions() diff --git a/src/backend/kernel_tools/generic_tools.py b/src/backend/kernel_tools/generic_tools.py deleted file mode 100644 index 7aa14b260..000000000 --- a/src/backend/kernel_tools/generic_tools.py +++ /dev/null @@ -1,133 +0,0 @@ -import inspect -from typing import Callable - -from semantic_kernel.functions import kernel_function -from common.models.messages_kernel import AgentType -import json -from typing import get_type_hints - - -class GenericTools: - """Define Generic Agent functions (tools)""" - - agent_name = AgentType.GENERIC.value - - @staticmethod - @kernel_function( - description="This is a placeholder function, for a proper Azure AI Search RAG process." - ) - async def dummy_function() -> str: - # This is a placeholder function, for a proper Azure AI Search RAG process. - - """This is a placeholder""" - return "This is a placeholder function" - - @classmethod - def get_all_kernel_functions(cls) -> dict[str, Callable]: - """ - Returns a dictionary of all methods in this class that have the @kernel_function annotation. - This function itself is not annotated with @kernel_function. - - Returns: - Dict[str, Callable]: Dictionary with function names as keys and function objects as values - """ - kernel_functions = {} - - # Get all class methods - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private/special methods - if name.startswith("_") or name == "get_all_kernel_functions": - continue - - # Check if the method has the kernel_function annotation - # by looking at its __annotations__ attribute - method_attrs = getattr(method, "__annotations__", {}) - if hasattr(method, "__kernel_function__") or "kernel_function" in str( - method_attrs - ): - kernel_functions[name] = method - - return kernel_functions - - @classmethod - def generate_tools_json_doc(cls) -> str: - """ - Generate a JSON document containing information about all methods in the class. - - Returns: - str: JSON string containing the methods' information - """ - - tools_list = [] - - # Get all methods from the class that have the kernel_function annotation - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private methods - if name.startswith("_") or name == "generate_tools_json_doc": - continue - - # Check if the method has the kernel_function annotation - if hasattr(method, "__kernel_function__"): - # Get method description from docstring or kernel_function description - description = "" - if hasattr(method, "__doc__") and method.__doc__: - description = method.__doc__.strip() - - # Get kernel_function description if available - if hasattr(method, "__kernel_function__") and getattr( - method.__kernel_function__, "description", None - ): - description = method.__kernel_function__.description - - # Get argument information by introspection - sig = inspect.signature(method) - args_dict = {} - - # Get type hints if available - type_hints = get_type_hints(method) - - # Process parameters - for param_name, param in sig.parameters.items(): - # Skip first parameter 'cls' for class methods (though we're using staticmethod now) - if param_name in ["cls", "self"]: - continue - - # Get parameter type - param_type = "string" # Default type - if param_name in type_hints: - type_obj = type_hints[param_name] - # Convert type to string representation - if hasattr(type_obj, "__name__"): - param_type = type_obj.__name__.lower() - else: - # Handle complex types like List, Dict, etc. - param_type = str(type_obj).lower() - if "int" in param_type: - param_type = "int" - elif "float" in param_type: - param_type = "float" - elif "bool" in param_type: - param_type = "boolean" - else: - param_type = "string" - - # Create parameter description - # param_desc = param_name.replace("_", " ") - args_dict[param_name] = { - "description": param_name, - "title": param_name.replace("_", " ").title(), - "type": param_type, - } - - # Add the tool information to the list - tool_entry = { - "agent": cls.agent_name, # Use HR agent type - "function": name, - "description": description, - "arguments": json.dumps(args_dict).replace('"', "'"), - } - - tools_list.append(tool_entry) - - # Return the JSON string representation - return json.dumps(tools_list, ensure_ascii=False) diff --git a/src/backend/kernel_tools/hr_tools.py b/src/backend/kernel_tools/hr_tools.py deleted file mode 100644 index 76643e6f7..000000000 --- a/src/backend/kernel_tools/hr_tools.py +++ /dev/null @@ -1,488 +0,0 @@ -import inspect -from typing import Annotated, Callable - -from semantic_kernel.functions import kernel_function -from common.models.messages_kernel import AgentType -import json -from typing import get_type_hints -from common.config.app_config import config - - -class HrTools: - # Define HR tools (functions) - selecetd_language = config.get_user_local_browser_language() - formatting_instructions = "Instructions: returning the output of this function call verbatim to the user in markdown. Then write AGENT SUMMARY: and then include a summary of what you did. Convert all date strings in the following text to short date format with 3-letter month (MMM) in the {selecetd_language} locale (e.g., en-US, en-IN), remove time, and replace original dates with the formatted ones" - agent_name = AgentType.HR.value - - @staticmethod - @kernel_function(description="Schedule an orientation session for a new employee.") - async def schedule_orientation_session(employee_name: str, date: str) -> str: - - return ( - f"##### Orientation Session Scheduled\n" - f"**Employee Name:** {employee_name}\n" - f"**Date:** {date}\n\n" - f"Your orientation session has been successfully scheduled. " - f"Please mark your calendar and be prepared for an informative session.\n" - f"{HrTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Assign a mentor to a new employee.") - async def assign_mentor(employee_name: str) -> str: - return ( - f"##### Mentor Assigned\n" - f"**Employee Name:** {employee_name}\n\n" - f"A mentor has been assigned to you. They will guide you through your onboarding process and help you settle into your new role.\n" - f"{HrTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Register a new employee for benefits.") - async def register_for_benefits(employee_name: str) -> str: - return ( - f"##### Benefits Registration\n" - f"**Employee Name:** {employee_name}\n\n" - f"You have been successfully registered for benefits. " - f"Please review your benefits package and reach out if you have any questions.\n" - f"{HrTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Enroll an employee in a training program.") - async def enroll_in_training_program(employee_name: str, program_name: str) -> str: - return ( - f"##### Training Program Enrollment\n" - f"**Employee Name:** {employee_name}\n" - f"**Program Name:** {program_name}\n\n" - f"You have been enrolled in the training program. " - f"Please check your email for further details and instructions.\n" - f"{HrTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Provide the employee handbook to a new employee.") - async def provide_employee_handbook(employee_name: str) -> str: - return ( - f"##### Employee Handbook Provided\n" - f"**Employee Name:** {employee_name}\n\n" - f"The employee handbook has been provided to you. " - f"Please review it to familiarize yourself with company policies and procedures.\n" - f"{HrTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Update a specific field in an employee's record.") - async def update_employee_record(employee_name: str, field: str, value: str) -> str: - return ( - f"##### Employee Record Updated\n" - f"**Employee Name:** {employee_name}\n" - f"**Field Updated:** {field}\n" - f"**New Value:** {value}\n\n" - f"Your employee record has been successfully updated.\n" - f"{HrTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Request an ID card for a new employee.") - async def request_id_card(employee_name: str) -> str: - return ( - f"##### ID Card Request\n" - f"**Employee Name:** {employee_name}\n\n" - f"Your request for an ID card has been successfully submitted. " - f"Please allow 3-5 business days for processing. You will be notified once your ID card is ready for pickup.\n" - f"{HrTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Set up payroll for a new employee.") - async def set_up_payroll(employee_name: str) -> str: - return ( - f"##### Payroll Setup\n" - f"**Employee Name:** {employee_name}\n\n" - f"Your payroll has been successfully set up. " - f"Please review your payroll details and ensure everything is correct.\n" - f"{HrTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Add emergency contact information for an employee.") - async def add_emergency_contact( - employee_name: str, contact_name: str, contact_phone: str - ) -> str: - return ( - f"##### Emergency Contact Added\n" - f"**Employee Name:** {employee_name}\n" - f"**Contact Name:** {contact_name}\n" - f"**Contact Phone:** {contact_phone}\n\n" - f"Your emergency contact information has been successfully added.\n" - f"{HrTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Process a leave request for an employee.") - # async def process_leave_request( - # employee_name: str, leave_type: str, start_date: str, end_date: str - # ) -> str: - # return ( - # f"##### Leave Request Processed\n" - # f"**Employee Name:** {employee_name}\n" - # f"**Leave Type:** {leave_type}\n" - # f"**Start Date:** {start_date}\n" - # f"**End Date:** {end_date}\n\n" - # f"Your leave request has been processed. " - # f"Please ensure you have completed any necessary handover tasks before your leave.\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Update company policies.") - # async def update_policies(policy_name: str, policy_content: str) -> str: - # return ( - # f"##### Policy Updated\n" - # f"**Policy Name:** {policy_name}\n\n" - # f"The policy has been updated with the following content:\n\n" - # f"{policy_content}\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function( - # description="Conduct an exit interview for an employee leaving the company." - # ) - # async def conduct_exit_interview(employee_name: str) -> str: - # return ( - # f"##### Exit Interview Conducted\n" - # f"**Employee Name:** {employee_name}\n\n" - # f"The exit interview has been conducted. " - # f"Thank you for your feedback and contributions to the company.\n" - # f"{HrTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Verify employment status for an employee.") - async def verify_employment(employee_name: str) -> str: - return ( - f"##### Employment Verification\n" - f"**Employee Name:** {employee_name}\n\n" - f"The employment status of {employee_name} has been verified.\n" - f"{HrTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Schedule a performance review for an employee.") - # async def schedule_performance_review(employee_name: str, date: str) -> str: - # return ( - # f"##### Performance Review Scheduled\n" - # f"**Employee Name:** {employee_name}\n" - # f"**Date:** {date}\n\n" - # f"Your performance review has been scheduled. " - # f"Please prepare any necessary documents and be ready for the review.\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Approve an expense claim for an employee.") - # async def approve_expense_claim(employee_name: str, claim_amount: float) -> str: - # return ( - # f"##### Expense Claim Approved\n" - # f"**Employee Name:** {employee_name}\n" - # f"**Claim Amount:** ${claim_amount:.2f}\n\n" - # f"Your expense claim has been approved. " - # f"The amount will be reimbursed in your next payroll.\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Send a company-wide announcement.") - # async def send_company_announcement(subject: str, content: str) -> str: - # return ( - # f"##### Company Announcement\n" - # f"**Subject:** {subject}\n\n" - # f"{content}\n" - # f"{HrTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Retrieve the employee directory.") - async def fetch_employee_directory() -> str: - return ( - f"##### Employee Directory\n\n" - f"The employee directory has been retrieved.\n" - f"{HrTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function( - description="Get HR information, such as policies, procedures, and onboarding guidelines." - ) - async def get_hr_information( - query: Annotated[str, "The query for the HR knowledgebase"], - ) -> str: - information = ( - f"##### HR Information\n\n" - f"**Document Name:** Contoso's Employee Onboarding Procedure\n" - f"**Domain:** HR Policy\n" - f"**Description:** A step-by-step guide detailing the onboarding process for new Contoso employees, from initial orientation to role-specific training.\n" - f"{HrTools.formatting_instructions}" - ) - return information - - # Additional HR tools - @staticmethod - @kernel_function(description="Initiate a background check for a new employee.") - async def initiate_background_check(employee_name: str) -> str: - return ( - f"##### Background Check Initiated\n" - f"**Employee Name:** {employee_name}\n\n" - f"A background check has been initiated for {employee_name}. " - f"You will be notified once the check is complete.\n" - f"{HrTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Organize a team-building activity.") - async def organize_team_building_activity(activity_name: str, date: str) -> str: - return ( - f"##### Team-Building Activity Organized\n" - f"**Activity Name:** {activity_name}\n" - f"**Date:** {date}\n\n" - f"The team-building activity has been successfully organized. " - f"Please join us on {date} for a fun and engaging experience.\n" - f"{HrTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Manage an employee transfer between departments.") - # async def manage_employee_transfer(employee_name: str, new_department: str) -> str: - # return ( - # f"##### Employee Transfer\n" - # f"**Employee Name:** {employee_name}\n" - # f"**New Department:** {new_department}\n\n" - # f"The transfer has been successfully processed. " - # f"{employee_name} is now part of the {new_department} department.\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Track attendance for an employee.") - # async def track_employee_attendance(employee_name: str) -> str: - # return ( - # f"##### Attendance Tracked\n" - # f"**Employee Name:** {employee_name}\n\n" - # f"The attendance for {employee_name} has been successfully tracked.\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Organize a health and wellness program.") - # async def organize_wellness_program(program_name: str, date: str) -> str: - # return ( - # f"##### Health and Wellness Program Organized\n" - # f"**Program Name:** {program_name}\n" - # f"**Date:** {date}\n\n" - # f"The health and wellness program has been successfully organized. " - # f"Please join us on {date} for an informative and engaging session.\n" - # f"{HrTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function( - description="Facilitate the setup for remote work for an employee." - ) - async def facilitate_remote_work_setup(employee_name: str) -> str: - return ( - f"##### Remote Work Setup Facilitated\n" - f"**Employee Name:** {employee_name}\n\n" - f"The remote work setup has been successfully facilitated for {employee_name}. " - f"Please ensure you have all the necessary equipment and access.\n" - f"{HrTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Manage the retirement plan for an employee.") - # async def manage_retirement_plan(employee_name: str) -> str: - # return ( - # f"##### Retirement Plan Managed\n" - # f"**Employee Name:** {employee_name}\n\n" - # f"The retirement plan for {employee_name} has been successfully managed.\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Handle an overtime request for an employee.") - # async def handle_overtime_request(employee_name: str, hours: float) -> str: - # return ( - # f"##### Overtime Request Handled\n" - # f"**Employee Name:** {employee_name}\n" - # f"**Hours:** {hours}\n\n" - # f"The overtime request for {employee_name} has been successfully handled.\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Issue a bonus to an employee.") - # async def issue_bonus(employee_name: str, amount: float) -> str: - # return ( - # f"##### Bonus Issued\n" - # f"**Employee Name:** {employee_name}\n" - # f"**Amount:** ${amount:.2f}\n\n" - # f"A bonus of ${amount:.2f} has been issued to {employee_name}.\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Schedule a wellness check for an employee.") - # async def schedule_wellness_check(employee_name: str, date: str) -> str: - # return ( - # f"##### Wellness Check Scheduled\n" - # f"**Employee Name:** {employee_name}\n" - # f"**Date:** {date}\n\n" - # f"A wellness check has been scheduled for {employee_name} on {date}.\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Handle a suggestion made by an employee.") - # async def handle_employee_suggestion(employee_name: str, suggestion: str) -> str: - # return ( - # f"##### Employee Suggestion Handled\n" - # f"**Employee Name:** {employee_name}\n" - # f"**Suggestion:** {suggestion}\n\n" - # f"The suggestion from {employee_name} has been successfully handled.\n" - # f"{HrTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Update privileges for an employee.") - # async def update_employee_privileges( - # employee_name: str, privilege: str, status: str - # ) -> str: - # return ( - # f"##### Employee Privileges Updated\n" - # f"**Employee Name:** {employee_name}\n" - # f"**Privilege:** {privilege}\n" - # f"**Status:** {status}\n\n" - # f"The privileges for {employee_name} have been successfully updated.\n" - # f"{HrTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Send a welcome email to an address.") - async def send_email(emailaddress: str) -> str: - return ( - f"##### Welcome Email Sent\n" - f"**Email Address:** {emailaddress}\n\n" - f"A welcome email has been sent to {emailaddress}.\n" - f"{HrTools.formatting_instructions}" - ) - - @classmethod - def get_all_kernel_functions(cls) -> dict[str, Callable]: - """ - Returns a dictionary of all methods in this class that have the @kernel_function annotation. - This function itself is not annotated with @kernel_function. - - Returns: - Dict[str, Callable]: Dictionary with function names as keys and function objects as values - """ - kernel_functions = {} - - # Get all class methods - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private/special methods - if name.startswith("_") or name == "get_all_kernel_functions": - continue - - # Check if the method has the kernel_function annotation - # by looking at its __annotations__ attribute - method_attrs = getattr(method, "__annotations__", {}) - if hasattr(method, "__kernel_function__") or "kernel_function" in str( - method_attrs - ): - kernel_functions[name] = method - - return kernel_functions - - @classmethod - def generate_tools_json_doc(cls) -> str: - """ - Generate a JSON document containing information about all methods in the class. - - Returns: - str: JSON string containing the methods' information - """ - - tools_list = [] - - # Get all methods from the class that have the kernel_function annotation - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private methods - if name.startswith("_") or name == "generate_tools_json_doc": - continue - - # Check if the method has the kernel_function annotation - if hasattr(method, "__kernel_function__"): - # Get method description from docstring or kernel_function description - description = "" - if hasattr(method, "__doc__") and method.__doc__: - description = method.__doc__.strip() - - # Get kernel_function description if available - if hasattr(method, "__kernel_function__") and getattr( - method.__kernel_function__, "description", None - ): - description = method.__kernel_function__.description - - # Get argument information by introspection - sig = inspect.signature(method) - args_dict = {} - - # Get type hints if available - type_hints = get_type_hints(method) - - # Process parameters - for param_name, param in sig.parameters.items(): - # Skip first parameter 'cls' for class methods (though we're using staticmethod now) - if param_name in ["cls", "self"]: - continue - - # Get parameter type - param_type = "string" # Default type - if param_name in type_hints: - type_obj = type_hints[param_name] - # Convert type to string representation - if hasattr(type_obj, "__name__"): - param_type = type_obj.__name__.lower() - else: - # Handle complex types like List, Dict, etc. - param_type = str(type_obj).lower() - if "int" in param_type: - param_type = "int" - elif "float" in param_type: - param_type = "float" - elif "bool" in param_type: - param_type = "boolean" - else: - param_type = "string" - - # Create parameter description - # param_desc = param_name.replace("_", " ") - args_dict[param_name] = { - "description": param_name, - "title": param_name.replace("_", " ").title(), - "type": param_type, - } - - # Add the tool information to the list - tool_entry = { - "agent": cls.agent_name, # Use HR agent type - "function": name, - "description": description, - "arguments": json.dumps(args_dict).replace('"', "'"), - } - - tools_list.append(tool_entry) - - # Return the JSON string representation - return json.dumps(tools_list, ensure_ascii=False) diff --git a/src/backend/kernel_tools/marketing_tools.py b/src/backend/kernel_tools/marketing_tools.py deleted file mode 100644 index 250191faa..000000000 --- a/src/backend/kernel_tools/marketing_tools.py +++ /dev/null @@ -1,392 +0,0 @@ -"""MarketingTools class provides various marketing functions for a marketing agent.""" - -import inspect -import json -from typing import Callable, List, get_type_hints - -from semantic_kernel.functions import kernel_function -from common.models.messages_kernel import AgentType - - -class MarketingTools: - """A class that provides various marketing tools and functions.""" - - agent_name = AgentType.MARKETING.value - - @staticmethod - @kernel_function(description="Create a new marketing campaign.") - async def create_marketing_campaign( - campaign_name: str, target_audience: str, budget: float - ) -> str: - return f"Marketing campaign '{campaign_name}' created targeting '{target_audience}' with a budget of ${budget:.2f}." - - @staticmethod - @kernel_function(description="Analyze market trends in a specific industry.") - async def analyze_market_trends(industry: str) -> str: - return f"Market trends analyzed for the '{industry}' industry." - - # ToDo: Seems to be a bug in SK when processing functions with list parameters - @staticmethod - @kernel_function(description="Generate social media posts for a campaign.") - async def generate_social_posts(campaign_name: str, platforms: List[str]) -> str: - platforms_str = ", ".join(platforms) - return f"Social media posts for campaign '{campaign_name}' generated for platforms: {platforms_str}." - - @staticmethod - @kernel_function(description="Plan the advertising budget for a campaign.") - async def plan_advertising_budget(campaign_name: str, total_budget: float) -> str: - return f"Advertising budget planned for campaign '{campaign_name}' with a total budget of ${total_budget:.2f}." - - # @staticmethod - # @kernel_function(description="Conduct a customer survey on a specific topic.") - # async def conduct_customer_survey(survey_topic: str, target_group: str) -> str: - # return ( - # f"Customer survey on '{survey_topic}' conducted targeting '{target_group}'." - # ) - - @staticmethod - @kernel_function(description="Perform a competitor analysis.") - async def perform_competitor_analysis(competitor_name: str) -> str: - return f"Competitor analysis performed on '{competitor_name}'." - - # @staticmethod - # @kernel_function(description="Schedule a marketing event.") - # async def schedule_marketing_event( - # event_name: str, date: str, location: str - # ) -> str: - # return f"Marketing event '{event_name}' scheduled on {date} at {location}." - - @staticmethod - @kernel_function(description="Design promotional material for a campaign.") - async def design_promotional_material( - campaign_name: str, material_type: str - ) -> str: - return f"{material_type.capitalize()} for campaign '{campaign_name}' designed." - - @staticmethod - @kernel_function(description="Manage email marketing for a campaign.") - async def manage_email_marketing(campaign_name: str, email_list_size: int) -> str: - return f"Email marketing managed for campaign '{campaign_name}' targeting {email_list_size} recipients." - - # @staticmethod - # @kernel_function(description="Track the performance of a campaign.") - # async def track_campaign_performance(campaign_name: str) -> str: - # return f"Performance of campaign '{campaign_name}' tracked." - - @staticmethod - @kernel_function(description="Coordinate a campaign with the sales team.") - async def coordinate_with_sales_team(campaign_name: str) -> str: - return f"Campaign '{campaign_name}' coordinated with the sales team." - - # @staticmethod - # @kernel_function(description="Develop a brand strategy.") - # async def develop_brand_strategy(brand_name: str) -> str: - # return f"Brand strategy developed for '{brand_name}'." - - # @staticmethod - # @kernel_function(description="Create a content calendar for a specific month.") - # async def create_content_calendar(month: str) -> str: - # return f"Content calendar for '{month}' created." - - # @staticmethod - # @kernel_function(description="Update content on a specific website page.") - # async def update_website_content(page_name: str) -> str: - # return f"Website content on page '{page_name}' updated." - - @staticmethod - @kernel_function(description="Plan a product launch.") - async def plan_product_launch(product_name: str, launch_date: str) -> str: - return f"Product launch for '{product_name}' planned on {launch_date}." - - @staticmethod - @kernel_function( - description="This is a function to draft / write a press release. You must call the function by passing the key information that you want to be included in the press release." - ) - async def generate_press_release(key_information_for_press_release: str) -> str: - return f"Look through the conversation history. Identify the content. Now you must generate a press release based on this content {key_information_for_press_release}. Make it approximately 2 paragraphs." - - # @staticmethod - # @kernel_function(description="Conduct market research on a specific topic.") - # async def conduct_market_research(research_topic: str) -> str: - # return f"Market research conducted on '{research_topic}'." - - # @staticmethod - # @kernel_function(description="Handle customer feedback.") - # async def handle_customer_feedback(feedback_details: str) -> str: - # return f"Customer feedback handled: {feedback_details}." - - @staticmethod - @kernel_function(description="Generate a marketing report for a campaign.") - async def generate_marketing_report(campaign_name: str) -> str: - return f"Marketing report generated for campaign '{campaign_name}'." - - # @staticmethod - # @kernel_function(description="Manage a social media account.") - # async def manage_social_media_account(platform: str, account_name: str) -> str: - # return ( - # f"Social media account '{account_name}' on platform '{platform}' managed." - # ) - - @staticmethod - @kernel_function(description="Create a video advertisement.") - async def create_video_ad(content_title: str, platform: str) -> str: - return ( - f"Video advertisement '{content_title}' created for platform '{platform}'." - ) - - # @staticmethod - # @kernel_function(description="Conduct a focus group study.") - # async def conduct_focus_group(study_topic: str, participants: int) -> str: - # return f"Focus group study on '{study_topic}' conducted with {participants} participants." - - # @staticmethod - # @kernel_function(description="Update brand guidelines.") - # async def update_brand_guidelines(brand_name: str, guidelines: str) -> str: - # return f"Brand guidelines for '{brand_name}' updated." - - @staticmethod - @kernel_function(description="Handle collaboration with an influencer.") - async def handle_influencer_collaboration( - influencer_name: str, campaign_name: str - ) -> str: - return f"Collaboration with influencer '{influencer_name}' for campaign '{campaign_name}' handled." - - # @staticmethod - # @kernel_function(description="Analyze customer behavior in a specific segment.") - # async def analyze_customer_behavior(segment: str) -> str: - # return f"Customer behavior in segment '{segment}' analyzed." - - # @staticmethod - # @kernel_function(description="Manage a customer loyalty program.") - # async def manage_loyalty_program(program_name: str, members: int) -> str: - # return f"Loyalty program '{program_name}' managed with {members} members." - - @staticmethod - @kernel_function(description="Develop a content strategy.") - async def develop_content_strategy(strategy_name: str) -> str: - return f"Content strategy '{strategy_name}' developed." - - # @staticmethod - # @kernel_function(description="Create an infographic.") - # async def create_infographic(content_title: str) -> str: - # return f"Infographic '{content_title}' created." - - # @staticmethod - # @kernel_function(description="Schedule a webinar.") - # async def schedule_webinar(webinar_title: str, date: str, platform: str) -> str: - # return f"Webinar '{webinar_title}' scheduled on {date} via {platform}." - - @staticmethod - @kernel_function(description="Manage online reputation for a brand.") - async def manage_online_reputation(brand_name: str) -> str: - return f"Online reputation for '{brand_name}' managed." - - @staticmethod - @kernel_function(description="Run A/B testing for an email campaign.") - async def run_email_ab_testing(campaign_name: str) -> str: - return f"A/B testing for email campaign '{campaign_name}' run." - - # @staticmethod - # @kernel_function(description="Create a podcast episode.") - # async def create_podcast_episode(series_name: str, episode_title: str) -> str: - # return f"Podcast episode '{episode_title}' for series '{series_name}' created." - - @staticmethod - @kernel_function(description="Manage an affiliate marketing program.") - async def manage_affiliate_program(program_name: str, affiliates: int) -> str: - return ( - f"Affiliate program '{program_name}' managed with {affiliates} affiliates." - ) - - # @staticmethod - # @kernel_function(description="Generate lead magnets.") - # async def generate_lead_magnets(content_title: str) -> str: - # return f"Lead magnet '{content_title}' generated." - - # @staticmethod - # @kernel_function(description="Organize participation in a trade show.") - # async def organize_trade_show(booth_number: str, event_name: str) -> str: - # return f"Trade show '{event_name}' organized at booth number '{booth_number}'." - - # @staticmethod - # @kernel_function(description="Manage a customer retention program.") - # async def manage_retention_program(program_name: str) -> str: - # return f"Customer retention program '{program_name}' managed." - - @staticmethod - @kernel_function(description="Run a pay-per-click (PPC) campaign.") - async def run_ppc_campaign(campaign_name: str, budget: float) -> str: - return f"PPC campaign '{campaign_name}' run with a budget of ${budget:.2f}." - - @staticmethod - @kernel_function(description="Create a case study.") - async def create_case_study(case_title: str, client_name: str) -> str: - return f"Case study '{case_title}' for client '{client_name}' created." - - # @staticmethod - # @kernel_function(description="Generate lead nurturing emails.") - # async def generate_lead_nurturing_emails(sequence_name: str, steps: int) -> str: - # return f"Lead nurturing email sequence '{sequence_name}' generated with {steps} steps." - - # @staticmethod - # @kernel_function(description="Manage crisis communication.") - # async def manage_crisis_communication(crisis_situation: str) -> str: - # return f"Crisis communication managed for situation '{crisis_situation}'." - - # @staticmethod - # @kernel_function(description="Create interactive content.") - # async def create_interactive_content(content_title: str) -> str: - # return f"Interactive content '{content_title}' created." - - # @staticmethod - # @kernel_function(description="Handle media relations.") - # async def handle_media_relations(media_outlet: str) -> str: - # return f"Media relations handled with '{media_outlet}'." - - @staticmethod - @kernel_function(description="Create a testimonial video.") - async def create_testimonial_video(client_name: str) -> str: - return f"Testimonial video created for client '{client_name}'." - - @staticmethod - @kernel_function(description="Manage event sponsorship.") - async def manage_event_sponsorship(event_name: str, sponsor_name: str) -> str: - return f"Event sponsorship for '{event_name}' managed with sponsor '{sponsor_name}'." - - # @staticmethod - # @kernel_function(description="Optimize a specific stage of the conversion funnel.") - # async def optimize_conversion_funnel(stage: str) -> str: - # return f"Conversion funnel stage '{stage}' optimized." - - # ToDo: Seems to be a bug in SK when processing functions with list parameters - @staticmethod - @kernel_function(description="Run an influencer marketing campaign.") - async def run_influencer_campaign( - campaign_name: str, influencers: List[str] - ) -> str: - influencers_str = ", ".join(influencers) - return f"Influencer marketing campaign '{campaign_name}' run with influencers: {influencers_str}." - - @staticmethod - @kernel_function(description="Analyze website traffic from a specific source.") - async def analyze_website_traffic(source: str) -> str: - return f"Website traffic analyzed from source '{source}'." - - @staticmethod - @kernel_function(description="Develop customer personas for a specific segment.") - async def develop_customer_personas(segment_name: str) -> str: - return f"Customer personas developed for segment '{segment_name}'." - - # This function does NOT have the kernel_function annotation - # because it's meant for introspection rather than being exposed as a tool - @classmethod - def generate_tools_json_doc(cls) -> str: - """ - Generate a JSON document containing information about all methods in the class. - - Returns: - str: JSON string containing the methods' information - """ - - tools_list = [] - - # Get all methods from the class that have the kernel_function annotation - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private methods - if name.startswith("_") or name == "generate_tools_json_doc": - continue - - # Check if the method has the kernel_function annotation - if hasattr(method, "__kernel_function__"): - # Get method description from docstring or kernel_function description - description = "" - if hasattr(method, "__doc__") and method.__doc__: - description = method.__doc__.strip() - - # Get kernel_function description if available - if hasattr(method, "__kernel_function__") and getattr( - method.__kernel_function__, "description", None - ): - description = method.__kernel_function__.description - - # Get argument information by introspection - sig = inspect.signature(method) - args_dict = {} - - # Get type hints if available - type_hints = get_type_hints(method) - - # Process parameters - for param_name, param in sig.parameters.items(): - # Skip first parameter 'cls' for class methods (though we're using staticmethod now) - if param_name in ["cls", "self"]: - continue - - # Get parameter type - param_type = "string" # Default type - if param_name in type_hints: - type_obj = type_hints[param_name] - # Convert type to string representation - if hasattr(type_obj, "__name__"): - param_type = type_obj.__name__.lower() - else: - # Handle complex types like List, Dict, etc. - param_type = str(type_obj).lower() - if "int" in param_type: - param_type = "int" - elif "float" in param_type: - param_type = "float" - elif "bool" in param_type: - param_type = "boolean" - else: - param_type = "string" - - # Create parameter description - # param_desc = param_name.replace("_", " ") - args_dict[param_name] = { - "description": param_name, - "title": param_name.replace("_", " ").title(), - "type": param_type, - } - - # Add the tool information to the list - tool_entry = { - "agent": cls.agent_name, # Use HR agent type - "function": name, - "description": description, - "arguments": json.dumps(args_dict).replace('"', "'"), - } - - tools_list.append(tool_entry) - - # Return the JSON string representation - return json.dumps(tools_list, ensure_ascii=False) - - # This function does NOT have the kernel_function annotation - # because it's meant for introspection rather than being exposed as a tool - @classmethod - def get_all_kernel_functions(cls) -> dict[str, Callable]: - """ - Returns a dictionary of all methods in this class that have the @kernel_function annotation. - This function itself is not annotated with @kernel_function. - - Returns: - Dict[str, Callable]: Dictionary with function names as keys and function objects as values - """ - kernel_functions = {} - - # Get all class methods - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private/special methods - if name.startswith("_") or name == "get_all_kernel_functions": - continue - - # Check if the method has the kernel_function annotation - # by looking at its __annotations__ attribute - method_attrs = getattr(method, "__annotations__", {}) - if hasattr(method, "__kernel_function__") or "kernel_function" in str( - method_attrs - ): - kernel_functions[name] = method - - return kernel_functions diff --git a/src/backend/kernel_tools/procurement_tools.py b/src/backend/kernel_tools/procurement_tools.py deleted file mode 100644 index 9b78e7e76..000000000 --- a/src/backend/kernel_tools/procurement_tools.py +++ /dev/null @@ -1,668 +0,0 @@ -import inspect -from typing import Annotated, Callable - -from semantic_kernel.functions import kernel_function -from common.models.messages_kernel import AgentType -import json -from typing import get_type_hints - - -class ProcurementTools: - - formatting_instructions = "Instructions: returning the output of this function call verbatim to the user in markdown. Then write AGENT SUMMARY: and then include a summary of what you did." - agent_name = AgentType.PROCUREMENT.value - - # Define Procurement tools (functions) - @staticmethod - @kernel_function(description="Order hardware items like laptops, monitors, etc.") - async def order_hardware(item_name: str, quantity: int) -> str: - return ( - f"##### Hardware Order Placed\n" - f"**Item:** {item_name}\n" - f"**Quantity:** {quantity}\n\n" - f"Ordered {quantity} units of {item_name}.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Order software licenses.") - async def order_software_license( - software_name: str, license_type: str, quantity: int - ) -> str: - return ( - f"##### Software License Ordered\n" - f"**Software:** {software_name}\n" - f"**License Type:** {license_type}\n" - f"**Quantity:** {quantity}\n\n" - f"Ordered {quantity} {license_type} licenses of {software_name}.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Check the inventory status of an item.") - async def check_inventory(item_name: str) -> str: - return ( - f"##### Inventory Status\n" - f"**Item:** {item_name}\n" - f"**Status:** In Stock\n\n" - f"Inventory status of {item_name}: In Stock.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Process a purchase order.") - # async def process_purchase_order(po_number: str) -> str: - # return ( - # f"##### Purchase Order Processed\n" - # f"**PO Number:** {po_number}\n\n" - # f"Purchase Order {po_number} has been processed.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Initiate contract negotiation with a vendor.") - # async def initiate_contract_negotiation( - # vendor_name: str, contract_details: str - # ) -> str: - # return ( - # f"##### Contract Negotiation Initiated\n" - # f"**Vendor:** {vendor_name}\n" - # f"**Contract Details:** {contract_details}\n\n" - # f"Contract negotiation initiated with {vendor_name}: {contract_details}\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Approve an invoice for payment.") - # async def approve_invoice(invoice_number: str) -> str: - # return ( - # f"##### Invoice Approved\n" - # f"**Invoice Number:** {invoice_number}\n\n" - # f"Invoice {invoice_number} approved for payment.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Track the status of an order.") - # async def track_order(order_number: str) -> str: - # return ( - # f"##### Order Tracking\n" - # f"**Order Number:** {order_number}\n" - # f"**Status:** In Transit\n\n" - # f"Order {order_number} is currently in transit.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Manage relationships with vendors.") - # async def manage_vendor_relationship(vendor_name: str, action: str) -> str: - # return ( - # f"##### Vendor Relationship Update\n" - # f"**Vendor:** {vendor_name}\n" - # f"**Action:** {action}\n\n" - # f"Vendor relationship with {vendor_name} has been {action}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Update a procurement policy.") - async def update_procurement_policy(policy_name: str, policy_content: str) -> str: - return ( - f"##### Procurement Policy Updated\n" - f"**Policy:** {policy_name}\n\n" - f"Procurement policy '{policy_name}' updated.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Generate a procurement report.") - # async def generate_procurement_report(report_type: str) -> str: - # return ( - # f"##### Procurement Report Generated\n" - # f"**Report Type:** {report_type}\n\n" - # f"Generated {report_type} procurement report.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Evaluate the performance of a supplier.") - # async def evaluate_supplier_performance(supplier_name: str) -> str: - # return ( - # f"##### Supplier Performance Evaluation\n" - # f"**Supplier:** {supplier_name}\n\n" - # f"Performance evaluation for supplier {supplier_name} completed.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Handle the return of procured items.") - async def handle_return(item_name: str, quantity: int, reason: str) -> str: - return ( - f"##### Return Handled\n" - f"**Item:** {item_name}\n" - f"**Quantity:** {quantity}\n" - f"**Reason:** {reason}\n\n" - f"Processed return of {quantity} units of {item_name} due to {reason}.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Process payment to a vendor.") - async def process_payment(vendor_name: str, amount: float) -> str: - return ( - f"##### Payment Processed\n" - f"**Vendor:** {vendor_name}\n" - f"**Amount:** ${amount:.2f}\n\n" - f"Processed payment of ${amount:.2f} to {vendor_name}.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Request a quote for items.") - async def request_quote(item_name: str, quantity: int) -> str: - return ( - f"##### Quote Requested\n" - f"**Item:** {item_name}\n" - f"**Quantity:** {quantity}\n\n" - f"Requested quote for {quantity} units of {item_name}.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Recommend sourcing options for an item.") - # async def recommend_sourcing_options(item_name: str) -> str: - # return ( - # f"##### Sourcing Options\n" - # f"**Item:** {item_name}\n\n" - # f"Sourcing options for {item_name} have been provided.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function( - # description="Update the asset register with new or disposed assets." - # ) - # async def update_asset_register(asset_name: str, asset_details: str) -> str: - # return ( - # f"##### Asset Register Updated\n" - # f"**Asset:** {asset_name}\n" - # f"**Details:** {asset_details}\n\n" - # f"Asset register updated for {asset_name}: {asset_details}\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Manage leasing agreements for assets.") - # async def manage_leasing_agreements(agreement_details: str) -> str: - # return ( - # f"##### Leasing Agreement Managed\n" - # f"**Agreement Details:** {agreement_details}\n\n" - # f"Leasing agreement processed: {agreement_details}\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Conduct market research for procurement purposes.") - # async def conduct_market_research(category: str) -> str: - # return ( - # f"##### Market Research Conducted\n" - # f"**Category:** {category}\n\n" - # f"Market research conducted for category: {category}\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Schedule maintenance for equipment.") - # async def schedule_maintenance(equipment_name: str, maintenance_date: str) -> str: - # return ( - # f"##### Maintenance Scheduled\n" - # f"**Equipment:** {equipment_name}\n" - # f"**Date:** {maintenance_date}\n\n" - # f"Scheduled maintenance for {equipment_name} on {maintenance_date}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Conduct an inventory audit.") - async def audit_inventory() -> str: - return ( - f"##### Inventory Audit\n\n" - f"Inventory audit has been conducted.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Approve a procurement budget.") - # async def approve_budget(budget_id: str, amount: float) -> str: - # return ( - # f"##### Budget Approved\n" - # f"**Budget ID:** {budget_id}\n" - # f"**Amount:** ${amount:.2f}\n\n" - # f"Approved budget ID {budget_id} for amount ${amount:.2f}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Manage warranties for procured items.") - # async def manage_warranty(item_name: str, warranty_period: str) -> str: - # return ( - # f"##### Warranty Management\n" - # f"**Item:** {item_name}\n" - # f"**Warranty Period:** {warranty_period}\n\n" - # f"Warranty for {item_name} managed for period {warranty_period}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function( - # description="Handle customs clearance for international shipments." - # ) - # async def handle_customs_clearance(shipment_id: str) -> str: - # return ( - # f"##### Customs Clearance\n" - # f"**Shipment ID:** {shipment_id}\n\n" - # f"Customs clearance for shipment ID {shipment_id} handled.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Negotiate a discount with a vendor.") - # async def negotiate_discount(vendor_name: str, discount_percentage: float) -> str: - # return ( - # f"##### Discount Negotiated\n" - # f"**Vendor:** {vendor_name}\n" - # f"**Discount:** {discount_percentage}%\n\n" - # f"Negotiated a {discount_percentage}% discount with vendor {vendor_name}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Register a new vendor.") - # async def register_new_vendor(vendor_name: str, vendor_details: str) -> str: - # return ( - # f"##### New Vendor Registered\n" - # f"**Vendor:** {vendor_name}\n" - # f"**Details:** {vendor_details}\n\n" - # f"New vendor {vendor_name} registered with details: {vendor_details}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Decommission an asset.") - async def decommission_asset(asset_name: str) -> str: - return ( - f"##### Asset Decommissioned\n" - f"**Asset:** {asset_name}\n\n" - f"Asset {asset_name} has been decommissioned.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Schedule a training session for procurement staff.") - # async def schedule_training(session_name: str, date: str) -> str: - # return ( - # f"##### Training Session Scheduled\n" - # f"**Session:** {session_name}\n" - # f"**Date:** {date}\n\n" - # f"Training session '{session_name}' scheduled on {date}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Update the rating of a vendor.") - # async def update_vendor_rating(vendor_name: str, rating: float) -> str: - # return ( - # f"##### Vendor Rating Updated\n" - # f"**Vendor:** {vendor_name}\n" - # f"**Rating:** {rating}\n\n" - # f"Vendor {vendor_name} rating updated to {rating}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Handle the recall of a procured item.") - async def handle_recall(item_name: str, recall_reason: str) -> str: - return ( - f"##### Item Recall Handled\n" - f"**Item:** {item_name}\n" - f"**Reason:** {recall_reason}\n\n" - f"Recall of {item_name} due to {recall_reason} handled.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Request samples of an item.") - # async def request_samples(item_name: str, quantity: int) -> str: - # return ( - # f"##### Samples Requested\n" - # f"**Item:** {item_name}\n" - # f"**Quantity:** {quantity}\n\n" - # f"Requested {quantity} samples of {item_name}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Manage subscriptions to services.") - # async def manage_subscription(service_name: str, action: str) -> str: - # return ( - # f"##### Subscription Management\n" - # f"**Service:** {service_name}\n" - # f"**Action:** {action}\n\n" - # f"Subscription to {service_name} has been {action}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Verify the certification status of a supplier.") - # async def verify_supplier_certification(supplier_name: str) -> str: - # return ( - # f"##### Supplier Certification Verified\n" - # f"**Supplier:** {supplier_name}\n\n" - # f"Certification status of supplier {supplier_name} verified.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Conduct an audit of a supplier.") - # async def conduct_supplier_audit(supplier_name: str) -> str: - # return ( - # f"##### Supplier Audit Conducted\n" - # f"**Supplier:** {supplier_name}\n\n" - # f"Audit of supplier {supplier_name} conducted.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Manage import licenses for items.") - # async def manage_import_licenses(item_name: str, license_details: str) -> str: - # return ( - # f"##### Import License Management\n" - # f"**Item:** {item_name}\n" - # f"**License Details:** {license_details}\n\n" - # f"Import license for {item_name} managed: {license_details}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Conduct a cost analysis for an item.") - async def conduct_cost_analysis(item_name: str) -> str: - return ( - f"##### Cost Analysis Conducted\n" - f"**Item:** {item_name}\n\n" - f"Cost analysis for {item_name} conducted.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function( - description="Evaluate risk factors associated with procuring an item." - ) - async def evaluate_risk_factors(item_name: str) -> str: - return ( - f"##### Risk Factors Evaluated\n" - f"**Item:** {item_name}\n\n" - f"Risk factors for {item_name} evaluated.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Manage green procurement policy.") - # async def manage_green_procurement_policy(policy_details: str) -> str: - # return ( - # f"##### Green Procurement Policy Management\n" - # f"**Details:** {policy_details}\n\n" - # f"Green procurement policy managed: {policy_details}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Update the supplier database with new information.") - async def update_supplier_database(supplier_name: str, supplier_info: str) -> str: - return ( - f"##### Supplier Database Updated\n" - f"**Supplier:** {supplier_name}\n" - f"**Information:** {supplier_info}\n\n" - f"Supplier database updated for {supplier_name}: {supplier_info}.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Handle dispute resolution with a vendor.") - # async def handle_dispute_resolution(vendor_name: str, issue: str) -> str: - # return ( - # f"##### Dispute Resolution\n" - # f"**Vendor:** {vendor_name}\n" - # f"**Issue:** {issue}\n\n" - # f"Dispute with vendor {vendor_name} over issue '{issue}' resolved.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Assess compliance of an item with standards.") - # async def assess_compliance(item_name: str, compliance_standards: str) -> str: - # return ( - # f"##### Compliance Assessment\n" - # f"**Item:** {item_name}\n" - # f"**Standards:** {compliance_standards}\n\n" - # f"Compliance of {item_name} with standards '{compliance_standards}' assessed.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Manage reverse logistics for returning items.") - # async def manage_reverse_logistics(item_name: str, quantity: int) -> str: - # return ( - # f"##### Reverse Logistics Management\n" - # f"**Item:** {item_name}\n" - # f"**Quantity:** {quantity}\n\n" - # f"Reverse logistics managed for {quantity} units of {item_name}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Verify delivery status of an item.") - # async def verify_delivery(item_name: str, delivery_status: str) -> str: - # return ( - # f"##### Delivery Status Verification\n" - # f"**Item:** {item_name}\n" - # f"**Status:** {delivery_status}\n\n" - # f"Delivery status of {item_name} verified as {delivery_status}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="assess procurement risk assessment.") - async def assess_procurement_risk(risk_details: str) -> str: - return ( - f"##### Procurement Risk Assessment\n" - f"**Details:** {risk_details}\n\n" - f"Procurement risk assessment handled: {risk_details}.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Manage supplier contract actions.") - # async def manage_supplier_contract(supplier_name: str, contract_action: str) -> str: - # return ( - # f"##### Supplier Contract Management\n" - # f"**Supplier:** {supplier_name}\n" - # f"**Action:** {contract_action}\n\n" - # f"Supplier contract with {supplier_name} has been {contract_action}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Allocate budget to a department.") - # async def allocate_budget(department_name: str, budget_amount: float) -> str: - # return ( - # f"##### Budget Allocation\n" - # f"**Department:** {department_name}\n" - # f"**Amount:** ${budget_amount:.2f}\n\n" - # f"Allocated budget of ${budget_amount:.2f} to {department_name}.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - # @staticmethod - # @kernel_function(description="Track procurement metrics.") - # async def track_procurement_metrics(metric_name: str) -> str: - # return ( - # f"##### Procurement Metrics Tracking\n" - # f"**Metric:** {metric_name}\n\n" - # f"Procurement metric '{metric_name}' tracked.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function(description="Manage inventory levels for an item.") - async def manage_inventory_levels(item_name: str, action: str) -> str: - return ( - f"##### Inventory Level Management\n" - f"**Item:** {item_name}\n" - f"**Action:** {action}\n\n" - f"Inventory levels for {item_name} have been {action}.\n" - f"{ProcurementTools.formatting_instructions}" - ) - - # @staticmethod - # @kernel_function(description="Conduct a survey of a supplier.") - # async def conduct_supplier_survey(supplier_name: str) -> str: - # return ( - # f"##### Supplier Survey Conducted\n" - # f"**Supplier:** {supplier_name}\n\n" - # f"Survey of supplier {supplier_name} conducted.\n" - # f"{ProcurementTools.formatting_instructions}" - # ) - - @staticmethod - @kernel_function( - description="Get procurement information, such as policies, procedures, and guidelines." - ) - async def get_procurement_information( - query: Annotated[str, "The query for the procurement knowledgebase"], - ) -> str: - information = ( - f"##### Procurement Information\n\n" - f"**Document Name:** Contoso's Procurement Policies and Procedures\n" - f"**Domain:** Procurement Policy\n" - f"**Description:** Guidelines outlining the procurement processes for Contoso, including vendor selection, purchase orders, and asset management.\n\n" - f"**Key points:**\n" - f"- All hardware and software purchases must be approved by the procurement department.\n" - f"- For new employees, hardware requests (like laptops) and ID badges should be ordered through the procurement agent.\n" - f"- Software licenses should be managed to ensure compliance with vendor agreements.\n" - f"- Regular inventory checks should be conducted to maintain optimal stock levels.\n" - f"- Vendor relationships should be managed to achieve cost savings and ensure quality.\n" - f"{ProcurementTools.formatting_instructions}" - ) - return information - - @classmethod - def generate_tools_json_doc(cls) -> str: - """ - Generate a JSON document containing information about all methods in the class. - - Returns: - str: JSON string containing the methods' information - """ - - tools_list = [] - - # Get all methods from the class that have the kernel_function annotation - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private methods - if name.startswith("_") or name == "generate_tools_json_doc": - continue - - # Check if the method has the kernel_function annotation - if hasattr(method, "__kernel_function__"): - # Get method description from docstring or kernel_function description - description = "" - if hasattr(method, "__doc__") and method.__doc__: - description = method.__doc__.strip() - - # Get kernel_function description if available - if hasattr(method, "__kernel_function__") and getattr( - method.__kernel_function__, "description", None - ): - description = method.__kernel_function__.description - - # Get argument information by introspection - sig = inspect.signature(method) - args_dict = {} - - # Get type hints if available - type_hints = get_type_hints(method) - - # Process parameters - for param_name, param in sig.parameters.items(): - # Skip first parameter 'cls' for class methods (though we're using staticmethod now) - if param_name in ["cls", "self"]: - continue - - # Get parameter type - param_type = "string" # Default type - if param_name in type_hints: - type_obj = type_hints[param_name] - # Convert type to string representation - if hasattr(type_obj, "__name__"): - param_type = type_obj.__name__.lower() - else: - # Handle complex types like List, Dict, etc. - param_type = str(type_obj).lower() - if "int" in param_type: - param_type = "int" - elif "float" in param_type: - param_type = "float" - elif "bool" in param_type: - param_type = "boolean" - else: - param_type = "string" - - # Create parameter description - # param_desc = param_name.replace("_", " ") - args_dict[param_name] = { - "description": param_name, - "title": param_name.replace("_", " ").title(), - "type": param_type, - } - - # Add the tool information to the list - tool_entry = { - "agent": cls.agent_name, # Use HR agent type - "function": name, - "description": description, - "arguments": json.dumps(args_dict).replace('"', "'"), - } - - tools_list.append(tool_entry) - - # Return the JSON string representation - return json.dumps(tools_list, ensure_ascii=False) - - # This function does NOT have the kernel_function annotation - # because it's meant for introspection rather than being exposed as a tool - @classmethod - def get_all_kernel_functions(cls) -> dict[str, Callable]: - """ - Returns a dictionary of all methods in this class that have the @kernel_function annotation. - This function itself is not annotated with @kernel_function. - - Returns: - Dict[str, Callable]: Dictionary with function names as keys and function objects as values - """ - kernel_functions = {} - - # Get all class methods - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private/special methods - if name.startswith("_") or name == "get_all_kernel_functions": - continue - - # Check if the method has the kernel_function annotation - # by looking at its __annotations__ attribute - method_attrs = getattr(method, "__annotations__", {}) - if hasattr(method, "__kernel_function__") or "kernel_function" in str( - method_attrs - ): - kernel_functions[name] = method - - return kernel_functions diff --git a/src/backend/kernel_tools/product_tools.py b/src/backend/kernel_tools/product_tools.py deleted file mode 100644 index fbe3f7989..000000000 --- a/src/backend/kernel_tools/product_tools.py +++ /dev/null @@ -1,724 +0,0 @@ -"""ProductTools class for managing product-related tasks in a mobile plan context.""" - -import inspect -import time -from typing import Annotated, Callable, List - -from semantic_kernel.functions import kernel_function -from common.models.messages_kernel import AgentType -import json -from typing import get_type_hints -from common.utils.utils_date import format_date_for_user -from common.config.app_config import config - - -class ProductTools: - """Define Product Agent functions (tools)""" - - agent_name = AgentType.PRODUCT.value - selecetd_language = config.get_user_local_browser_language() - - @staticmethod - @kernel_function( - description="Add an extras pack/new product to the mobile plan for the customer. For example, adding a roaming plan to their service. Convert all date strings in the following text to short date format with 3-letter month (MMM) in the {selecetd_language} locale (e.g., en-US, en-IN), remove time, and replace original dates with the formatted ones" - ) - async def add_mobile_extras_pack(new_extras_pack_name: str, start_date: str) -> str: - """Add an extras pack/new product to the mobile plan for the customer. For example, adding a roaming plan to their service. The arguments should include the new_extras_pack_name and the start_date as strings. You must provide the exact plan name, as found using the get_product_info() function.""" - formatting_instructions = "Instructions: returning the output of this function call verbatim to the user in markdown. Then write AGENT SUMMARY: and then include a summary of what you did." - analysis = ( - f"# Request to Add Extras Pack to Mobile Plan\n" - f"## New Plan:\n{new_extras_pack_name}\n" - f"## Start Date:\n{start_date}\n\n" - f"These changes have been completed and should be reflected in your app in 5-10 minutes." - f"\n\n{formatting_instructions}" - ) - time.sleep(2) - return analysis - - @staticmethod - @kernel_function( - description="Get information about available products and phone plans, including roaming services." - ) - async def get_product_info() -> str: - # This is a placeholder function, for a proper Azure AI Search RAG process. - - """Get information about the different products and phone plans available, including roaming services.""" - product_info = """ - - # Simulated Phone Plans - - ## Plan A: Basic Saver - - **Monthly Cost**: $25 - - **Data**: 5GB - - **Calls**: Unlimited local calls - - **Texts**: Unlimited local texts - - ## Plan B: Standard Plus - - **Monthly Cost**: $45 - - **Data**: 15GB - - **Calls**: Unlimited local and national calls - - **Texts**: Unlimited local and national texts - - ## Plan C: Premium Unlimited - - **Monthly Cost**: $70 - - **Data**: Unlimited - - **Calls**: Unlimited local, national, and international calls - - **Texts**: Unlimited local, national, and international texts - - # Roaming Extras Add-On Pack - - **Cost**: $15/month - - **Data**: 1GB - - **Calls**: 200 minutes - - **Texts**: 200 texts - - """ - return f"Here is information to relay back to the user. Repeat back all the relevant sections that the user asked for: {product_info}." - - # @staticmethod - # @kernel_function( - # description="Retrieve the customer's recurring billing date information." - # ) - # async def get_billing_date() -> str: - # """Get information about the recurring billing date.""" - # now = datetime.now() - # start_of_month = datetime(now.year, now.month, 1) - # start_of_month_string = start_of_month.strftime("%Y-%m-%d") - # formatted_date = format_date_for_user(start_of_month_string) - # return f"## Billing Date\nYour most recent billing date was **{formatted_date}**." - - @staticmethod - @kernel_function( - description="Check the current inventory level for a specified product." - ) - async def check_inventory(product_name: str) -> str: - """Check the inventory level for a specific product.""" - inventory_status = ( - f"## Inventory Status\nInventory status for **'{product_name}'** checked." - ) - return inventory_status - - @staticmethod - @kernel_function( - description="Update the inventory quantity for a specified product." - ) - async def update_inventory(product_name: str, quantity: int) -> str: - """Update the inventory quantity for a specific product.""" - message = f"## Inventory Update\nInventory for **'{product_name}'** updated by **{quantity}** units." - - return message - - @staticmethod - @kernel_function( - description="Add a new product to the inventory system with detailed product information." - ) - async def add_new_product( - product_details: Annotated[str, "Details of the new product"], - ) -> str: - """Add a new product to the inventory.""" - message = f"## New Product Added\nNew product added with details:\n\n{product_details}" - - return message - - # @staticmethod - # @kernel_function( - # description="Update the price of a specified product in the system." - # ) - # async def update_product_price(product_name: str, price: float) -> str: - # """Update the price of a specific product.""" - # message = f"## Price Update\nPrice for **'{product_name}'** updated to **${price:.2f}**." - - # return message - - @staticmethod - @kernel_function(description="Schedule a product launch event on a specific date.") - async def schedule_product_launch(product_name: str, launch_date: str) -> str: - """Schedule a product launch on a specific date.""" - formatted_date = format_date_for_user(launch_date) - message = f"## Product Launch Scheduled\nProduct **'{product_name}'** launch scheduled on **{formatted_date}**." - - return message - - # @staticmethod - # @kernel_function( - # description="Analyze sales data for a product over a specified time period." - # ) - # async def analyze_sales_data(product_name: str, time_period: str) -> str: - # """Analyze sales data for a product over a given time period.""" - # analysis = f"## Sales Data Analysis\nSales data for **'{product_name}'** over **{time_period}** analyzed." - - # return analysis - - # @staticmethod - # @kernel_function(description="Retrieve customer feedback for a specified product.") - # async def get_customer_feedback(product_name: str) -> str: - # """Retrieve customer feedback for a specific product.""" - # feedback = f"## Customer Feedback\nCustomer feedback for **'{product_name}'** retrieved." - - # return feedback - - @staticmethod - @kernel_function( - description="Manage promotional activities for a specified product." - ) - async def manage_promotions( - product_name: str, - promotion_details: Annotated[str, "Details of the promotion"], - ) -> str: - """Manage promotions for a specific product.""" - message = f"## Promotion Managed\nPromotion for **'{product_name}'** managed with details:\n\n{promotion_details}" - - return message - - @staticmethod - @kernel_function( - description="Coordinate with the marketing team for product campaign activities." - ) - async def coordinate_with_marketing( - product_name: str, - campaign_details: Annotated[str, "Details of the marketing campaign"], - ) -> str: - """Coordinate with the marketing team for a product.""" - message = f"## Marketing Coordination\nCoordinated with marketing for **'{product_name}'** campaign:\n\n{campaign_details}" - - return message - - # @staticmethod - # @kernel_function( - # description="Review and assess the quality of a specified product." - # ) - # async def review_product_quality(product_name: str) -> str: - # """Review the quality of a specific product.""" - # review = ( - # f"## Quality Review\nQuality review for **'{product_name}'** completed." - # ) - - # return review - - @staticmethod - @kernel_function( - description="Initiate and manage a product recall for a specified product." - ) - async def handle_product_recall(product_name: str, recall_reason: str) -> str: - """Handle a product recall for a specific product.""" - message = f"## Product Recall\nProduct recall for **'{product_name}'** initiated due to:\n\n{recall_reason}" - - return message - - # @staticmethod - # @kernel_function( - # description="Provide product recommendations based on customer preferences." - # ) - # async def provide_product_recommendations( - # customer_preferences: Annotated[str, "Customer preferences or requirements"], - # ) -> str: - # """Provide product recommendations based on customer preferences.""" - # recommendations = f"## Product Recommendations\nProduct recommendations based on preferences **'{customer_preferences}'** provided." - - # return recommendations - - @staticmethod - @kernel_function(description="Generate a detailed report for a specified product.") - async def generate_product_report(product_name: str, report_type: str) -> str: - """Generate a report for a specific product.""" - report = f"## {report_type} Report\n{report_type} report for **'{product_name}'** generated." - - return report - - # @staticmethod - # @kernel_function( - # description="Manage supply chain activities for a specified product with a particular supplier." - # ) - # async def manage_supply_chain(product_name: str, supplier_name: str) -> str: - # """Manage supply chain activities for a specific product.""" - # message = f"## Supply Chain Management\nSupply chain for **'{product_name}'** managed with supplier **'{supplier_name}'**." - - # return message - - # @staticmethod - # @kernel_function( - # description="Track the shipment status of a specified product using a tracking number." - # ) - # async def track_product_shipment(product_name: str, tracking_number: str) -> str: - # """Track the shipment of a specific product.""" - # status = f"## Shipment Tracking\nShipment for **'{product_name}'** with tracking number **'{tracking_number}'** tracked." - - # return status - - # @staticmethod - # @kernel_function( - # description="Set the reorder threshold level for a specified product." - # ) - # async def set_reorder_level(product_name: str, reorder_level: int) -> str: - # """Set the reorder level for a specific product.""" - # message = f"## Reorder Level Set\nReorder level for **'{product_name}'** set to **{reorder_level}** units." - - # return message - - @staticmethod - @kernel_function( - description="Monitor and analyze current market trends relevant to product lines." - ) - async def monitor_market_trends() -> str: - """Monitor market trends relevant to products.""" - trends = "## Market Trends\nMarket trends monitored and data updated." - - return trends - - @staticmethod - @kernel_function(description="Develop and document new product ideas and concepts.") - async def develop_new_product_ideas( - idea_details: Annotated[str, "Details of the new product idea"], - ) -> str: - """Develop new product ideas.""" - message = f"## New Product Idea\nNew product idea developed:\n\n{idea_details}" - - return message - - @staticmethod - @kernel_function( - description="Collaborate with the technical team for product development and specifications." - ) - async def collaborate_with_tech_team( - product_name: str, - collaboration_details: Annotated[str, "Details of the technical requirements"], - ) -> str: - """Collaborate with the tech team for product development.""" - message = f"## Tech Team Collaboration\nCollaborated with tech team on **'{product_name}'**:\n\n{collaboration_details}" - - return message - - # @staticmethod - # @kernel_function( - # description="Update the description information for a specified product." - # ) - # async def update_product_description(product_name: str, description: str) -> str: - # """Update the description of a specific product.""" - # message = f"## Product Description Updated\nDescription for **'{product_name}'** updated to:\n\n{description}" - - # return message - - # @staticmethod - # @kernel_function(description="Set a percentage discount for a specified product.") - # async def set_product_discount( - # product_name: str, discount_percentage: float - # ) -> str: - # """Set a discount for a specific product.""" - # message = f"## Discount Set\nDiscount for **'{product_name}'** set to **{discount_percentage}%**." - - # return message - - # @staticmethod - # @kernel_function( - # description="Process and manage product returns with detailed reason tracking." - # ) - # async def manage_product_returns(product_name: str, return_reason: str) -> str: - # """Manage returns for a specific product.""" - # message = f"## Product Return Managed\nReturn for **'{product_name}'** managed due to:\n\n{return_reason}" - - # return message - - # @staticmethod - # @kernel_function(description="Conduct a customer survey about a specified product.") - # async def conduct_product_survey(product_name: str, survey_details: str) -> str: - # """Conduct a survey for a specific product.""" - # message = f"## Product Survey Conducted\nSurvey for **'{product_name}'** conducted with details:\n\n{survey_details}" - - # return message - - # @staticmethod - # @kernel_function( - # description="Handle and process customer complaints about a specified product." - # ) - # async def handle_product_complaints( - # product_name: str, complaint_details: str - # ) -> str: - # """Handle complaints for a specific product.""" - # message = f"## Product Complaint Handled\nComplaint for **'{product_name}'** handled with details:\n\n{complaint_details}" - - # return message - - # @staticmethod - # @kernel_function( - # description="Update the technical specifications for a specified product." - # ) - # async def update_product_specifications( - # product_name: str, specifications: str - # ) -> str: - # """Update the specifications for a specific product.""" - # message = f"## Product Specifications Updated\nSpecifications for **'{product_name}'** updated to:\n\n{specifications}" - - # return message - - @staticmethod - @kernel_function( - description="Organize and schedule a photoshoot for a specified product." - ) - async def organize_product_photoshoot( - product_name: str, photoshoot_date: str - ) -> str: - """Organize a photoshoot for a specific product.""" - message = f"## Product Photoshoot Organized\nPhotoshoot for **'{product_name}'** organized on **{photoshoot_date}**." - - return message - - @staticmethod - @kernel_function( - description="Manage the e-commerce platform listings for a specified product." - ) - async def manage_product_listing(product_name: str, listing_details: str) -> str: - """Manage the listing of a specific product on e-commerce platforms.""" - message = f"## Product Listing Managed\nListing for **'{product_name}'** managed with details:\n\n{listing_details}" - - return message - - # @staticmethod - # @kernel_function(description="Set the availability status of a specified product.") - # async def set_product_availability(product_name: str, availability: bool) -> str: - # """Set the availability status of a specific product.""" - # status = "available" if availability else "unavailable" - # message = f"## Product Availability Set\nProduct **'{product_name}'** is now **{status}**." - - # return message - - @staticmethod - @kernel_function( - description="Coordinate logistics operations for a specified product." - ) - async def coordinate_with_logistics( - product_name: str, logistics_details: str - ) -> str: - """Coordinate with the logistics team for a specific product.""" - message = f"## Logistics Coordination\nCoordinated with logistics for **'{product_name}'** with details:\n\n{logistics_details}" - - return message - - # @staticmethod - # @kernel_function( - # description="Calculate the profit margin for a specified product using cost and selling prices." - # ) - # async def calculate_product_margin( - # product_name: str, cost_price: float, selling_price: float - # ) -> str: - # """Calculate the profit margin for a specific product.""" - # margin = ((selling_price - cost_price) / selling_price) * 100 - # message = f"## Profit Margin Calculated\nProfit margin for **'{product_name}'** calculated at **{margin:.2f}%**." - - # return message - - @staticmethod - @kernel_function( - description="Update the category classification for a specified product." - ) - async def update_product_category(product_name: str, category: str) -> str: - """Update the category of a specific product.""" - message = f"## Product Category Updated\nCategory for **'{product_name}'** updated to:\n\n{category}" - - return message - - # @staticmethod - # @kernel_function( - # description="Create and manage product bundles with multiple products." - # ) - # async def manage_product_bundles(bundle_name: str, product_list: List[str]) -> str: - # """Manage product bundles.""" - # products = ", ".join(product_list) - # message = f"## Product Bundle Managed\nProduct bundle **'{bundle_name}'** managed with products:\n\n{products}" - - # return message - - # @staticmethod - # @kernel_function( - # description="Optimize the product page for better user experience and performance." - # ) - # async def optimize_product_page( - # product_name: str, optimization_details: str - # ) -> str: - # """Optimize the product page for better performance.""" - # message = f"## Product Page Optimized\nProduct page for **'{product_name}'** optimized with details:\n\n{optimization_details}" - - # return message - - # @staticmethod - # @kernel_function( - # description="Monitor and track performance metrics for a specified product." - # ) - # async def monitor_product_performance(product_name: str) -> str: - # """Monitor the performance of a specific product.""" - # message = f"## Product Performance Monitored\nPerformance for **'{product_name}'** monitored." - - # return message - - @staticmethod - @kernel_function( - description="Implement pricing strategies for a specified product." - ) - async def handle_product_pricing(product_name: str, pricing_strategy: str) -> str: - """Handle pricing strategy for a specific product.""" - message = f"## Pricing Strategy Set\nPricing strategy for **'{product_name}'** set to:\n\n{pricing_strategy}" - - return message - - @staticmethod - @kernel_function(description="Develop training materials for a specified product.") - async def create_training_material( - product_name: str, training_material: str - ) -> str: - """Develop training material for a specific product.""" - message = f"## Training Material Developed\nTraining material for **'{product_name}'** developed:\n\n{training_material}" - - return message - - # @staticmethod - # @kernel_function( - # description="Update the labeling information for a specified product." - # ) - # async def update_product_labels(product_name: str, label_details: str) -> str: - # """Update labels for a specific product.""" - # message = f"## Product Labels Updated\nLabels for **'{product_name}'** updated with details:\n\n{label_details}" - - # return message - - # @staticmethod - # @kernel_function( - # description="Manage warranty terms and conditions for a specified product." - # ) - # async def manage_product_warranty(product_name: str, warranty_details: str) -> str: - # """Manage the warranty for a specific product.""" - # message = f"## Product Warranty Managed\nWarranty for **'{product_name}'** managed with details:\n\n{warranty_details}" - - # return message - - # @staticmethod - # @kernel_function( - # description="Forecast future demand for a specified product over a time period." - # ) - # async def forecast_product_demand(product_name: str, forecast_period: str) -> str: - # """Forecast demand for a specific product.""" - # message = f"## Demand Forecast\nDemand for **'{product_name}'** forecasted for **{forecast_period}**." - - # return message - - # @staticmethod - # @kernel_function( - # description="Handle licensing agreements and requirements for a specified product." - # ) - # async def handle_product_licensing( - # product_name: str, licensing_details: str - # ) -> str: - # """Handle licensing for a specific product.""" - # message = f"## Product Licensing Handled\nLicensing for **'{product_name}'** handled with details:\n\n{licensing_details}" - - # return message - - @staticmethod - @kernel_function( - description="Manage packaging specifications and designs for a specified product." - ) - async def manage_product_packaging( - product_name: str, packaging_details: str - ) -> str: - """Manage packaging for a specific product.""" - message = f"## Product Packaging Managed\nPackaging for **'{product_name}'** managed with details:\n\n{packaging_details}" - - return message - - @staticmethod - @kernel_function( - description="Set safety standards and compliance requirements for a specified product." - ) - async def set_product_safety_standards( - product_name: str, safety_standards: str - ) -> str: - """Set safety standards for a specific product.""" - message = f"## Safety Standards Set\nSafety standards for **'{product_name}'** set to:\n\n{safety_standards}" - - return message - - @staticmethod - @kernel_function( - description="Develop and implement new features for a specified product." - ) - async def develop_product_features(product_name: str, features_details: str) -> str: - """Develop new features for a specific product.""" - message = f"## New Features Developed\nNew features for **'{product_name}'** developed with details:\n\n{features_details}" - - return message - - @staticmethod - @kernel_function( - description="Evaluate product performance based on specified criteria." - ) - async def evaluate_product_performance( - product_name: str, evaluation_criteria: str - ) -> str: - """Evaluate the performance of a specific product.""" - message = f"## Product Performance Evaluated\nPerformance of **'{product_name}'** evaluated based on:\n\n{evaluation_criteria}" - - return message - - @staticmethod - @kernel_function( - description="Manage custom product orders with specific customer requirements." - ) - async def manage_custom_product_orders(order_details: str) -> str: - """Manage custom orders for a specific product.""" - message = f"## Custom Product Order Managed\nCustom product order managed with details:\n\n{order_details}" - - return message - - @staticmethod - @kernel_function( - description="Update the product images for a specified product with new image URLs." - ) - async def update_product_images(product_name: str, image_urls: List[str]) -> str: - """Update images for a specific product.""" - images = ", ".join(image_urls) - message = f"## Product Images Updated\nImages for **'{product_name}'** updated:\n\n{images}" - - return message - - @staticmethod - @kernel_function( - description="Handle product obsolescence and end-of-life procedures for a specified product." - ) - async def handle_product_obsolescence(product_name: str) -> str: - """Handle the obsolescence of a specific product.""" - message = f"## Product Obsolescence Handled\nObsolescence for **'{product_name}'** handled." - - return message - - @staticmethod - @kernel_function( - description="Manage stock keeping unit (SKU) information for a specified product." - ) - async def manage_product_sku(product_name: str, sku: str) -> str: - """Manage SKU for a specific product.""" - message = f"## SKU Managed\nSKU for **'{product_name}'** managed:\n\n{sku}" - - return message - - @staticmethod - @kernel_function( - description="Provide product training sessions with detailed training materials." - ) - async def provide_product_training( - product_name: str, training_session_details: str - ) -> str: - """Provide training for a specific product.""" - message = f"## Product Training Provided\nTraining for **'{product_name}'** provided with details:\n\n{training_session_details}" - - return message - - # This function does NOT have the kernel_function annotation - # because it's meant for introspection rather than being exposed as a tool - @classmethod - def generate_tools_json_doc(cls) -> str: - """ - Generate a JSON document containing information about all methods in the class. - - Returns: - str: JSON string containing the methods' information - """ - - tools_list = [] - - # Get all methods from the class that have the kernel_function annotation - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private methods - if name.startswith("_") or name == "generate_tools_json_doc": - continue - - # Check if the method has the kernel_function annotation - if hasattr(method, "__kernel_function__"): - # Get method description from docstring or kernel_function description - description = "" - if hasattr(method, "__doc__") and method.__doc__: - description = method.__doc__.strip() - - # Get kernel_function description if available - if hasattr(method, "__kernel_function__") and getattr( - method.__kernel_function__, "description", None - ): - description = method.__kernel_function__.description - - # Get argument information by introspection - sig = inspect.signature(method) - args_dict = {} - - # Get type hints if available - type_hints = get_type_hints(method) - - # Process parameters - for param_name, param in sig.parameters.items(): - # Skip first parameter 'cls' for class methods (though we're using staticmethod now) - if param_name in ["cls", "self"]: - continue - - # Get parameter type - param_type = "string" # Default type - if param_name in type_hints: - type_obj = type_hints[param_name] - # Convert type to string representation - if hasattr(type_obj, "__name__"): - param_type = type_obj.__name__.lower() - else: - # Handle complex types like List, Dict, etc. - param_type = str(type_obj).lower() - if "int" in param_type: - param_type = "int" - elif "float" in param_type: - param_type = "float" - elif "bool" in param_type: - param_type = "boolean" - else: - param_type = "string" - - # Create parameter description - # param_desc = param_name.replace("_", " ") - args_dict[param_name] = { - "description": param_name, - "title": param_name.replace("_", " ").title(), - "type": param_type, - } - - # Add the tool information to the list - tool_entry = { - "agent": cls.agent_name, # Use HR agent type - "function": name, - "description": description, - "arguments": json.dumps(args_dict).replace('"', "'"), - } - - tools_list.append(tool_entry) - - # Return the JSON string representation - return json.dumps(tools_list, ensure_ascii=False) - - # This function does NOT have the kernel_function annotation - # because it's meant for introspection rather than being exposed as a tool - @classmethod - def get_all_kernel_functions(cls) -> dict[str, Callable]: - """ - Returns a dictionary of all methods in this class that have the @kernel_function annotation. - This function itself is not annotated with @kernel_function. - - Returns: - Dict[str, Callable]: Dictionary with function names as keys and function objects as values - """ - kernel_functions = {} - - # Get all class methods - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private/special methods - if name.startswith("_") or name == "get_all_kernel_functions": - continue - - # Check if the method has the kernel_function annotation - # by looking at its __annotations__ attribute - method_attrs = getattr(method, "__annotations__", {}) - if hasattr(method, "__kernel_function__") or "kernel_function" in str( - method_attrs - ): - kernel_functions[name] = method - - return kernel_functions diff --git a/src/backend/kernel_tools/tech_support_tools.py b/src/backend/kernel_tools/tech_support_tools.py deleted file mode 100644 index 99660750b..000000000 --- a/src/backend/kernel_tools/tech_support_tools.py +++ /dev/null @@ -1,410 +0,0 @@ -import inspect -from typing import Callable, get_type_hints -import json - -from semantic_kernel.functions import kernel_function -from common.models.messages_kernel import AgentType - - -class TechSupportTools: - # Define Tech Support tools (functions) - formatting_instructions = "Instructions: returning the output of this function call verbatim to the user in markdown. Then write AGENT SUMMARY: and then include a summary of what you did." - agent_name = AgentType.TECH_SUPPORT.value - - @staticmethod - @kernel_function( - description="Send a welcome email to a new employee as part of onboarding." - ) - async def send_welcome_email(employee_name: str, email_address: str) -> str: - return ( - f"##### Welcome Email Sent\n" - f"**Employee Name:** {employee_name}\n" - f"**Email Address:** {email_address}\n\n" - f"A welcome email has been successfully sent to {employee_name} at {email_address}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Set up an Office 365 account for an employee.") - async def set_up_office_365_account(employee_name: str, email_address: str) -> str: - return ( - f"##### Office 365 Account Setup\n" - f"**Employee Name:** {employee_name}\n" - f"**Email Address:** {email_address}\n\n" - f"An Office 365 account has been successfully set up for {employee_name} at {email_address}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Configure a laptop for a new employee.") - async def configure_laptop(employee_name: str, laptop_model: str) -> str: - return ( - f"##### Laptop Configuration\n" - f"**Employee Name:** {employee_name}\n" - f"**Laptop Model:** {laptop_model}\n\n" - f"The laptop {laptop_model} has been successfully configured for {employee_name}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Reset the password for an employee.") - async def reset_password(employee_name: str) -> str: - return ( - f"##### Password Reset\n" - f"**Employee Name:** {employee_name}\n\n" - f"The password for {employee_name} has been successfully reset.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Set up VPN access for an employee.") - async def setup_vpn_access(employee_name: str) -> str: - return ( - f"##### VPN Access Setup\n" - f"**Employee Name:** {employee_name}\n\n" - f"VPN access has been successfully set up for {employee_name}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Assist in troubleshooting network issues reported.") - async def troubleshoot_network_issue(issue_description: str) -> str: - return ( - f"##### Network Issue Resolved\n" - f"**Issue Description:** {issue_description}\n\n" - f"The network issue described as '{issue_description}' has been successfully resolved.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Install software for an employee.") - async def install_software(employee_name: str, software_name: str) -> str: - return ( - f"##### Software Installation\n" - f"**Employee Name:** {employee_name}\n" - f"**Software Name:** {software_name}\n\n" - f"The software '{software_name}' has been successfully installed for {employee_name}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Update software for an employee.") - async def update_software(employee_name: str, software_name: str) -> str: - return ( - f"##### Software Update\n" - f"**Employee Name:** {employee_name}\n" - f"**Software Name:** {software_name}\n\n" - f"The software '{software_name}' has been successfully updated for {employee_name}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Manage data backup for an employee's device.") - async def manage_data_backup(employee_name: str) -> str: - return ( - f"##### Data Backup Managed\n" - f"**Employee Name:** {employee_name}\n\n" - f"Data backup has been successfully configured for {employee_name}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Handle a reported cybersecurity incident.") - async def handle_cybersecurity_incident(incident_details: str) -> str: - return ( - f"##### Cybersecurity Incident Handled\n" - f"**Incident Details:** {incident_details}\n\n" - f"The cybersecurity incident described as '{incident_details}' has been successfully handled.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function( - description="support procurement with technical specifications of equipment." - ) - async def support_procurement_tech(equipment_details: str) -> str: - return ( - f"##### Technical Specifications Provided\n" - f"**Equipment Details:** {equipment_details}\n\n" - f"Technical specifications for the following equipment have been provided: {equipment_details}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Collaborate with CodeAgent for code deployment.") - async def collaborate_code_deployment(project_name: str) -> str: - return ( - f"##### Code Deployment Collaboration\n" - f"**Project Name:** {project_name}\n\n" - f"Collaboration on the deployment of project '{project_name}' has been successfully completed.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Provide technical support for a marketing campaign.") - async def assist_marketing_tech(campaign_name: str) -> str: - return ( - f"##### Tech Support for Marketing Campaign\n" - f"**Campaign Name:** {campaign_name}\n\n" - f"Technical support has been successfully provided for the marketing campaign '{campaign_name}'.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Provide tech support for a new product launch.") - async def assist_product_launch(product_name: str) -> str: - return ( - f"##### Tech Support for Product Launch\n" - f"**Product Name:** {product_name}\n\n" - f"Technical support has been successfully provided for the product launch of '{product_name}'.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Implement and manage an IT policy.") - async def implement_it_policy(policy_name: str) -> str: - return ( - f"##### IT Policy Implemented\n" - f"**Policy Name:** {policy_name}\n\n" - f"The IT policy '{policy_name}' has been successfully implemented.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Manage cloud services used by the company.") - async def manage_cloud_service(service_name: str) -> str: - return ( - f"##### Cloud Service Managed\n" - f"**Service Name:** {service_name}\n\n" - f"The cloud service '{service_name}' has been successfully managed.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Configure a server.") - async def configure_server(server_name: str) -> str: - return ( - f"##### Server Configuration\n" - f"**Server Name:** {server_name}\n\n" - f"The server '{server_name}' has been successfully configured.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Grant database access to an employee.") - async def grant_database_access(employee_name: str, database_name: str) -> str: - return ( - f"##### Database Access Granted\n" - f"**Employee Name:** {employee_name}\n" - f"**Database Name:** {database_name}\n\n" - f"Access to the database '{database_name}' has been successfully granted to {employee_name}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Provide technical training on new tools.") - async def provide_tech_training(employee_name: str, tool_name: str) -> str: - return ( - f"##### Tech Training Provided\n" - f"**Employee Name:** {employee_name}\n" - f"**Tool Name:** {tool_name}\n\n" - f"Technical training on '{tool_name}' has been successfully provided to {employee_name}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function( - description="Resolve general technical issues reported by employees." - ) - async def resolve_technical_issue(issue_description: str) -> str: - return ( - f"##### Technical Issue Resolved\n" - f"**Issue Description:** {issue_description}\n\n" - f"The technical issue described as '{issue_description}' has been successfully resolved.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Configure a printer for an employee.") - async def configure_printer(employee_name: str, printer_model: str) -> str: - return ( - f"##### Printer Configuration\n" - f"**Employee Name:** {employee_name}\n" - f"**Printer Model:** {printer_model}\n\n" - f"The printer '{printer_model}' has been successfully configured for {employee_name}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Set up an email signature for an employee.") - async def set_up_email_signature(employee_name: str, signature: str) -> str: - return ( - f"##### Email Signature Setup\n" - f"**Employee Name:** {employee_name}\n" - f"**Signature:** {signature}\n\n" - f"The email signature for {employee_name} has been successfully set up as '{signature}'.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Configure a mobile device for an employee.") - async def configure_mobile_device(employee_name: str, device_model: str) -> str: - return ( - f"##### Mobile Device Configuration\n" - f"**Employee Name:** {employee_name}\n" - f"**Device Model:** {device_model}\n\n" - f"The mobile device '{device_model}' has been successfully configured for {employee_name}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Manage software licenses for a specific software.") - async def manage_software_licenses(software_name: str, license_count: int) -> str: - return ( - f"##### Software Licenses Managed\n" - f"**Software Name:** {software_name}\n" - f"**License Count:** {license_count}\n\n" - f"{license_count} licenses for the software '{software_name}' have been successfully managed.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Set up remote desktop access for an employee.") - async def set_up_remote_desktop(employee_name: str) -> str: - return ( - f"##### Remote Desktop Setup\n" - f"**Employee Name:** {employee_name}\n\n" - f"Remote desktop access has been successfully set up for {employee_name}.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Assist in troubleshooting hardware issues reported.") - async def troubleshoot_hardware_issue(issue_description: str) -> str: - return ( - f"##### Hardware Issue Resolved\n" - f"**Issue Description:** {issue_description}\n\n" - f"The hardware issue described as '{issue_description}' has been successfully resolved.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @staticmethod - @kernel_function(description="Manage network security protocols.") - async def manage_network_security() -> str: - return ( - f"##### Network Security Managed\n\n" - f"Network security protocols have been successfully managed.\n" - f"{TechSupportTools.formatting_instructions}" - ) - - @classmethod - def generate_tools_json_doc(cls) -> str: - """ - Generate a JSON document containing information about all methods in the class. - - Returns: - str: JSON string containing the methods' information - """ - - tools_list = [] - - # Get all methods from the class that have the kernel_function annotation - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private methods - if name.startswith("_") or name == "generate_tools_json_doc": - continue - - # Check if the method has the kernel_function annotation - if hasattr(method, "__kernel_function__"): - # Get method description from docstring or kernel_function description - description = "" - if hasattr(method, "__doc__") and method.__doc__: - description = method.__doc__.strip() - - # Get kernel_function description if available - if hasattr(method, "__kernel_function__") and getattr( - method.__kernel_function__, "description", None - ): - description = method.__kernel_function__.description - - # Get argument information by introspection - sig = inspect.signature(method) - args_dict = {} - - # Get type hints if available - type_hints = get_type_hints(method) - - # Process parameters - for param_name, param in sig.parameters.items(): - # Skip first parameter 'cls' for class methods (though we're using staticmethod now) - if param_name in ["cls", "self"]: - continue - - # Get parameter type - param_type = "string" # Default type - if param_name in type_hints: - type_obj = type_hints[param_name] - # Convert type to string representation - if hasattr(type_obj, "__name__"): - param_type = type_obj.__name__.lower() - else: - # Handle complex types like List, Dict, etc. - param_type = str(type_obj).lower() - if "int" in param_type: - param_type = "int" - elif "float" in param_type: - param_type = "float" - elif "bool" in param_type: - param_type = "boolean" - else: - param_type = "string" - - # Create parameter description - # param_desc = param_name.replace("_", " ") - args_dict[param_name] = { - "description": param_name, - "title": param_name.replace("_", " ").title(), - "type": param_type, - } - - # Add the tool information to the list - tool_entry = { - "agent": cls.agent_name, # Use HR agent type - "function": name, - "description": description, - "arguments": json.dumps(args_dict).replace('"', "'"), - } - - tools_list.append(tool_entry) - - # Return the JSON string representation - return json.dumps(tools_list, ensure_ascii=False) - - # This function does NOT have the kernel_function annotation - # because it's meant for introspection rather than being exposed as a tool - @classmethod - def get_all_kernel_functions(cls) -> dict[str, Callable]: - """ - Returns a dictionary of all methods in this class that have the @kernel_function annotation. - This function itself is not annotated with @kernel_function. - - Returns: - Dict[str, Callable]: Dictionary with function names as keys and function objects as values - """ - kernel_functions = {} - - # Get all class methods - for name, method in inspect.getmembers(cls, predicate=inspect.isfunction): - # Skip this method itself and any private/special methods - if name.startswith("_") or name == "get_all_kernel_functions": - continue - - # Check if the method has the kernel_function annotation - # by looking at its __annotations__ attribute - method_attrs = getattr(method, "__annotations__", {}) - if hasattr(method, "__kernel_function__") or "kernel_function" in str( - method_attrs - ): - kernel_functions[name] = method - - return kernel_functions diff --git a/src/backend/v3/api/router.py b/src/backend/v3/api/router.py index 77edc839c..8fca48eba 100644 --- a/src/backend/v3/api/router.py +++ b/src/backend/v3/api/router.py @@ -5,6 +5,8 @@ import uuid from typing import Optional +from common.utils.utils_date import format_dates_in_messages +from common.config.app_config import config import v3.models.messages as messages from auth.auth_utils import get_authenticated_user_details from common.database.database_factory import DatabaseFactory @@ -12,6 +14,7 @@ InputTask, Plan, PlanStatus, + PlanWithSteps, TeamSelectionRequest, ) from common.utils.event_utils import track_event_if_configured @@ -19,8 +22,6 @@ from fastapi import ( APIRouter, BackgroundTasks, - Depends, - FastAPI, File, HTTPException, Query, @@ -29,7 +30,6 @@ WebSocket, WebSocketDisconnect, ) -from kernel_agents.agent_factory import AgentFactory from semantic_kernel.agents.runtime import InProcessRuntime from v3.common.services.team_service import TeamService from v3.config.settings import ( @@ -276,12 +276,16 @@ async def process_request( plan_id = str(uuid.uuid4()) # Initialize memory store and service memory_store = await DatabaseFactory.get_database(user_id=user_id) + user_current_team = await memory_store.get_current_team(user_id=user_id) + team_id = None + if user_current_team: + team_id = user_current_team.team_id plan = Plan( id=plan_id, plan_id=plan_id, user_id=user_id, session_id=input_task.session_id, - team_id=None, # TODO add current_team_id + team_id=team_id, initial_goal=input_task.description, overall_status=PlanStatus.in_progress, ) @@ -294,7 +298,7 @@ async def process_request( "plan_id": plan.plan_id, "session_id": input_task.session_id, "user_id": user_id, - "team_id": "", # TODO add current_team_id + "team_id": team_id, # TODO add current_team_id "description": input_task.description, }, ) @@ -377,7 +381,7 @@ async def plan_approval( # ) try: plan = orchestration_config.plans[human_feedback.m_plan_id] - if hasattr(plan, 'plan_id'): + if hasattr(plan, "plan_id"): print( "Updated orchestration config:", orchestration_config.plans[human_feedback.m_plan_id], @@ -406,9 +410,8 @@ async def plan_approval( ) except Exception as e: logging.error(f"Error processing plan approval: {e}") - raise HTTPException( - status_code=500, detail="Internal server error" - ) + raise HTTPException(status_code=500, detail="Internal server error") + @app_v3.post("/user_clarification") async def user_clarification( @@ -859,37 +862,6 @@ async def delete_team_config(team_id: str, request: Request): raise HTTPException(status_code=500, detail="Internal server error occurred") -@app_v3.get("/model_deployments") -async def get_model_deployments(request: Request): - """ - Get information about available model deployments for debugging/validation. - - --- - tags: - - Model Validation - responses: - 200: - description: List of available model deployments - 401: - description: Missing or invalid user information - """ - # Validate user authentication - authenticated_user = get_authenticated_user_details(request_headers=request.headers) - user_id = authenticated_user["user_principal_id"] - if not user_id: - raise HTTPException( - status_code=401, detail="Missing or invalid user information" - ) - - try: - team_service = TeamService() - deployments = [] # await team_service.extract_models_from_agent() - summary = await team_service.get_deployment_status_summary() - return {"deployments": deployments, "summary": summary} - except Exception as e: - logging.error(f"Error retrieving model deployments: {str(e)}") - raise HTTPException(status_code=500, detail="Internal server error occurred") - @app_v3.post("/select_team") async def select_team(selection: TeamSelectionRequest, request: Request): @@ -980,33 +952,189 @@ async def select_team(selection: TeamSelectionRequest, request: Request): ) raise HTTPException(status_code=500, detail="Internal server error occurred") +# Get plans is called in the initial side rendering of the frontend +@app_v3.get("/plans") +async def get_plans(request: Request): + """ + Retrieve plans for the current user. + + --- + tags: + - Plans + parameters: + - name: session_id + in: query + type: string + required: false + description: Optional session ID to retrieve plans for a specific session + responses: + 200: + description: List of plans with steps for the user + schema: + type: array + items: + type: object + properties: + id: + type: string + description: Unique ID of the plan + session_id: + type: string + description: Session ID associated with the plan + initial_goal: + type: string + description: The initial goal derived from the user's input + overall_status: + type: string + description: Status of the plan (e.g., in_progress, completed) + steps: + type: array + items: + type: object + properties: + id: + type: string + description: Unique ID of the step + plan_id: + type: string + description: ID of the plan the step belongs to + action: + type: string + description: The action to be performed + agent: + type: string + description: The agent responsible for the step + status: + type: string + description: Status of the step (e.g., planned, approved, completed) + 400: + description: Missing or invalid user information + 404: + description: Plan not found + """ + + authenticated_user = get_authenticated_user_details(request_headers=request.headers) + user_id = authenticated_user["user_principal_id"] + if not user_id: + track_event_if_configured( + "UserIdNotFound", {"status_code": 400, "detail": "no user"} + ) + raise HTTPException(status_code=400, detail="no user") + + #### Replace the following with code to get plan run history from the database -@app_v3.get("/search_indexes") -async def get_search_indexes(request: Request): + # # Initialize memory context + memory_store = await DatabaseFactory.get_database(user_id=user_id) + + current_team = await memory_store.get_current_team(user_id=user_id) + if not current_team: + return [] + + all_plans = await memory_store.get_all_plans_by_team_id(team_id=current_team.id) + + return all_plans + + +# Get plans is called in the initial side rendering of the frontend +@app_v3.get("/plan") +async def get_plan_by_id(request: Request, plan_id: str): """ - Get information about available search indexes for debugging/validation. + Retrieve plans for the current user. --- tags: - - Search Validation + - Plans + parameters: + - name: session_id + in: query + type: string + required: false + description: Optional session ID to retrieve plans for a specific session responses: 200: - description: List of available search indexes - 401: + description: List of plans with steps for the user + schema: + type: array + items: + type: object + properties: + id: + type: string + description: Unique ID of the plan + session_id: + type: string + description: Session ID associated with the plan + initial_goal: + type: string + description: The initial goal derived from the user's input + overall_status: + type: string + description: Status of the plan (e.g., in_progress, completed) + steps: + type: array + items: + type: object + properties: + id: + type: string + description: Unique ID of the step + plan_id: + type: string + description: ID of the plan the step belongs to + action: + type: string + description: The action to be performed + agent: + type: string + description: The agent responsible for the step + status: + type: string + description: Status of the step (e.g., planned, approved, completed) + 400: description: Missing or invalid user information + 404: + description: Plan not found """ - # Validate user authentication + authenticated_user = get_authenticated_user_details(request_headers=request.headers) user_id = authenticated_user["user_principal_id"] if not user_id: - raise HTTPException( - status_code=401, detail="Missing or invalid user information" + track_event_if_configured( + "UserIdNotFound", {"status_code": 400, "detail": "no user"} ) + raise HTTPException(status_code=400, detail="no user") - try: - team_service = TeamService() - summary = await team_service.get_search_index_summary() - return {"search_summary": summary} - except Exception as e: - logging.error(f"Error retrieving search indexes: {str(e)}") - raise HTTPException(status_code=500, detail="Internal server error occurred") + #### Replace the following with code to get plan run history from the database + + # # Initialize memory context + memory_store = await DatabaseFactory.get_database(user_id=user_id) + + if plan_id: + plan = await memory_store.get_plan_by_plan_id(plan_id=plan_id) + if not plan: + track_event_if_configured( + "GetPlanBySessionNotFound", + {"status_code": 400, "detail": "Plan not found"}, + ) + raise HTTPException(status_code=404, detail="Plan not found") + + # Use get_steps_by_plan to match the original implementation + steps = await memory_store.get_steps_by_plan(plan_id=plan.id) + messages = await memory_store.get_data_by_type_and_session_id( + "agent_message", session_id=plan.session_id + ) + + plan_with_steps = PlanWithSteps(**plan.model_dump(), steps=steps) + plan_with_steps.update_step_counts() + + # Format dates in messages according to locale + formatted_messages = format_dates_in_messages( + messages, config.get_user_local_browser_language() + ) + + return [plan_with_steps, formatted_messages] + else: + track_event_if_configured( + "GetPlanId", {"status_code": 400, "detail": "no plan id"} + ) + raise HTTPException(status_code=400, detail="no plan id") diff --git a/src/backend/v3/callbacks/response_handlers.py b/src/backend/v3/callbacks/response_handlers.py index 0b723c0c6..ef4c24f72 100644 --- a/src/backend/v3/callbacks/response_handlers.py +++ b/src/backend/v3/callbacks/response_handlers.py @@ -12,7 +12,7 @@ StreamingChatMessageContent) from v3.config.settings import connection_config, current_user_id from v3.models.messages import (AgentMessage, AgentMessageStreaming, - AgentToolCall, AgentToolMessage) + AgentToolCall, AgentToolMessage, WebsocketMessageType) def agent_response_callback(message: ChatMessageContent, user_id: str = None) -> None: @@ -35,14 +35,16 @@ def agent_response_callback(message: ChatMessageContent, user_id: str = None) -> if item.content_type == 'function_call': tool_call = AgentToolCall(tool_name=item.name or "unknown_tool", arguments=item.arguments or {}) final_message.tool_calls.append(tool_call) - asyncio.create_task(connection_config.send_status_update_async(final_message, user_id)) + + asyncio.create_task(connection_config.send_status_update_async(final_message, user_id, message_type=WebsocketMessageType.AGENT_TOOL_MESSAGE)) logging.info(f"Function call: {final_message}") elif message.items and message.items[0].content_type == 'function_result': # skip returning these results for now - agent will return in a later message pass else: final_message = AgentMessage(agent_name=agent_name, timestamp=time.time() or "", content=message.content or "") - asyncio.create_task(connection_config.send_status_update_async(final_message, user_id)) + + asyncio.create_task(connection_config.send_status_update_async(final_message, user_id, message_type=WebsocketMessageType.AGENT_MESSAGE)) logging.info(f"{role.capitalize()} message: {final_message}") except Exception as e: logging.error(f"Response_callback: Error sending WebSocket message: {e}") @@ -54,6 +56,6 @@ async def streaming_agent_response_callback(streaming_message: StreamingChatMess if user_id: try: message = AgentMessageStreaming(agent_name=streaming_message.name or "Unknown Agent", content=streaming_message.content, is_final=is_final) - await connection_config.send_status_update_async(message, user_id) + await connection_config.send_status_update_async(message, user_id, message_type=WebsocketMessageType.AGENT_MESSAGE_STREAMING) except Exception as e: logging.error(f"Response_callback: Error sending streaming WebSocket message: {e}") \ No newline at end of file diff --git a/src/backend/v3/config/settings.py b/src/backend/v3/config/settings.py index 2a6d44ecb..cd04023ee 100644 --- a/src/backend/v3/config/settings.py +++ b/src/backend/v3/config/settings.py @@ -14,12 +14,19 @@ from fastapi import WebSocket from semantic_kernel.agents.orchestration.magentic import MagenticOrchestration from semantic_kernel.connectors.ai.open_ai import ( - AzureChatCompletion, OpenAIChatPromptExecutionSettings) + AzureChatCompletion, + OpenAIChatPromptExecutionSettings, +) + +from v3.models.messages import WebsocketMessageType logger = logging.getLogger(__name__) # Create a context variable to track current user -current_user_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar('current_user_id', default=None) +current_user_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar( + "current_user_id", default=None +) + class AzureConfig: """Azure OpenAI and authentication configuration.""" @@ -37,7 +44,7 @@ def ad_token_provider(self) -> str: token = self.credential.get_token(config.AZURE_COGNITIVE_SERVICES) return token.token - async def create_chat_completion_service(self, use_reasoning_model: bool=False): + async def create_chat_completion_service(self, use_reasoning_model: bool = False): """Create Azure Chat Completion service.""" model_name = ( self.reasoning_model if use_reasoning_model else self.standard_model @@ -75,16 +82,19 @@ class OrchestrationConfig: """Configuration for orchestration settings.""" def __init__(self): - self.orchestrations: Dict[str, MagenticOrchestration] = {} # user_id -> orchestration instance - self.plans: Dict[str, any] = {} # plan_id -> plan details - self.approvals: Dict[str, bool] = {} # m_plan_id -> approval status - self.sockets: Dict[str, WebSocket] = {} # user_id -> WebSocket - self.clarifications: Dict[str, str] = {} # m_plan_id -> clarification response + self.orchestrations: Dict[str, MagenticOrchestration] = ( + {} + ) # user_id -> orchestration instance + self.plans: Dict[str, any] = {} # plan_id -> plan details + self.approvals: Dict[str, bool] = {} # m_plan_id -> approval status + self.sockets: Dict[str, WebSocket] = {} # user_id -> WebSocket + self.clarifications: Dict[str, str] = {} # m_plan_id -> clarification response def get_current_orchestration(self, user_id: str) -> MagenticOrchestration: """get existing orchestration instance.""" return self.orchestrations.get(user_id, None) - + + class ConnectionConfig: """Connection manager for WebSocket connections.""" @@ -93,15 +103,19 @@ def __init__(self): # Map user_id to process_id for context-based messaging self.user_to_process: Dict[str, str] = {} - def add_connection(self, process_id: str, connection: WebSocket, user_id: str = None): + def add_connection( + self, process_id: str, connection: WebSocket, user_id: str = None + ): """Add a new connection.""" # Close existing connection if it exists if process_id in self.connections: try: asyncio.create_task(self.connections[process_id].close()) except Exception as e: - logger.error(f"Error closing existing connection for user {process_id}: {e}") - + logger.error( + f"Error closing existing connection for user {process_id}: {e}" + ) + self.connections[process_id] = connection # Map user to process for context-based messaging if user_id: @@ -114,12 +128,18 @@ def add_connection(self, process_id: str, connection: WebSocket, user_id: str = try: asyncio.create_task(old_connection.close()) del self.connections[old_process_id] - logger.info(f"Closed old connection {old_process_id} for user {user_id}") + logger.info( + f"Closed old connection {old_process_id} for user {user_id}" + ) except Exception as e: - logger.error(f"Error closing old connection for user {user_id}: {e}") - + logger.error( + f"Error closing old connection for user {user_id}: {e}" + ) + self.user_to_process[user_id] = process_id - logger.info(f"WebSocket connection added for process: {process_id} (user: {user_id})") + logger.info( + f"WebSocket connection added for process: {process_id} (user: {user_id})" + ) else: logger.info(f"WebSocket connection added for process: {process_id}") @@ -128,7 +148,7 @@ def remove_connection(self, process_id): process_id = str(process_id) if process_id in self.connections: del self.connections[process_id] - + # Remove from user mapping if exists for user_id, mapped_process_id in list(self.user_to_process.items()): if mapped_process_id == process_id: @@ -139,7 +159,7 @@ def remove_connection(self, process_id): def get_connection(self, process_id): """Get a connection.""" return self.connections.get(process_id) - + async def close_connection(self, process_id): """Remove a connection.""" connection = self.get_connection(process_id) @@ -156,26 +176,43 @@ async def close_connection(self, process_id): self.remove_connection(process_id) logger.info("Connection removed for batch ID: %s", process_id) - async def send_status_update_async(self, message: any, user_id: Optional[str] = None): + async def send_status_update_async( + self, + message: any, + user_id: Optional[str] = None, + message_type: WebsocketMessageType = WebsocketMessageType.SYSTEM_MESSAGE, + ): """Send a status update to a specific client.""" # If no process_id provided, get from context if user_id is None: user_id = current_user_id.get() - + if not user_id: logger.warning("No user_id available for WebSocket message") return - + process_id = self.user_to_process.get(user_id) if not process_id: logger.warning("No active WebSocket process found for user ID: %s", user_id) - logger.debug(f"Available user mappings: {list(self.user_to_process.keys())}") + logger.debug( + f"Available user mappings: {list(self.user_to_process.keys())}" + ) return - + + try: + if hasattr(message, "data") and hasattr(message, "type"): + message = message.data + except Exception as e: + print(f"Error loading message data: {e}") + + standard_message = { + "type": message_type, + "data": message + } connection = self.get_connection(process_id) if connection: try: - str_message = json.dumps(message, default=str) + str_message = json.dumps(standard_message, default=str) await connection.send_text(str_message) logger.debug(f"Message sent to user {user_id} via process {process_id}") except Exception as e: @@ -183,7 +220,9 @@ async def send_status_update_async(self, message: any, user_id: Optional[str] = # Clean up stale connection self.remove_connection(process_id) else: - logger.warning("No connection found for process ID: %s (user: %s)", process_id, user_id) + logger.warning( + "No connection found for process ID: %s (user: %s)", process_id, user_id + ) # Clean up stale mapping if user_id in self.user_to_process: del self.user_to_process[user_id] @@ -201,6 +240,7 @@ def send_status_update(self, message: str, process_id: str): else: logger.warning("No connection found for process ID: %s", process_id) + class TeamConfig: """Team configuration for agents.""" @@ -218,6 +258,7 @@ def get_current_team(self, user_id: str) -> TeamConfiguration: """Get the current team configuration.""" return self.teams.get(user_id, None) + # Global config instances azure_config = AzureConfig() mcp_config = MCPConfig() diff --git a/src/backend/v3/magentic_agents/proxy_agent.py b/src/backend/v3/magentic_agents/proxy_agent.py index 5e8836955..caf83716a 100644 --- a/src/backend/v3/magentic_agents/proxy_agent.py +++ b/src/backend/v3/magentic_agents/proxy_agent.py @@ -24,7 +24,7 @@ from v3.config.settings import (connection_config, current_user_id, orchestration_config) from v3.models.messages import (UserClarificationRequest, - UserClarificationResponse) + UserClarificationResponse, WebsocketMessageType) class DummyAgentThread(AgentThread): @@ -155,10 +155,10 @@ async def invoke(self, message: str,*, thread: AgentThread | None = None,**kwarg # Send the approval request to the user's WebSocket await connection_config.send_status_update_async({ - "type": "user_clarification_request", + "type": WebsocketMessageType.USER_CLARIFICATION_REQUEST, "data": clarification_message - }) - + }, user_id=current_user_id.get(), message_type=WebsocketMessageType.USER_CLARIFICATION_REQUEST) + # Get human input human_response = await self._wait_for_user_clarification(clarification_message.request_id) @@ -203,10 +203,10 @@ async def invoke_stream(self, messages, thread=None, **kwargs) -> AsyncIterator[ # Send the approval request to the user's WebSocket # The user_id will be automatically retrieved from context await connection_config.send_status_update_async({ - "type": "user_clarification_request", + "type": WebsocketMessageType.USER_CLARIFICATION_REQUEST, "data": clarification_message - }) - + }, user_id=current_user_id.get(), message_type=WebsocketMessageType.USER_CLARIFICATION_REQUEST) + # Get human input - replace with websocket call when available human_response = await self._wait_for_user_clarification(clarification_message.request_id) diff --git a/src/backend/v3/models/messages.py b/src/backend/v3/models/messages.py index c464f1538..9086aab3b 100644 --- a/src/backend/v3/models/messages.py +++ b/src/backend/v3/models/messages.py @@ -1,5 +1,6 @@ """Messages from the backend to the frontend via WebSocket.""" +from enum import Enum import uuid from dataclasses import asdict, dataclass, field from typing import Any, Dict, List, Literal, Optional @@ -135,4 +136,38 @@ class ApprovalRequest(KernelBaseModel): session_id: str user_id: str action: str - agent_name: str \ No newline at end of file + agent_name: str + + + +class WebsocketMessageType(str, Enum): + """Types of WebSocket messages.""" + SYSTEM_MESSAGE = "system_message" + AGENT_MESSAGE = "agent_message" + AGENT_STREAM_START = "agent_stream_start" + AGENT_STREAM_END = "agent_stream_end" + AGENT_MESSAGE_STREAMING = "agent_message_streaming" + AGENT_TOOL_MESSAGE = "agent_tool_message" + PLAN_APPROVAL_REQUEST = "plan_approval_request" + PLAN_APPROVAL_RESPONSE = "plan_approval_response" + REPLAN_APPROVAL_REQUEST = "replan_approval_request" + REPLAN_APPROVAL_RESPONSE = "replan_approval_response" + USER_CLARIFICATION_REQUEST = "user_clarification_request" + USER_CLARIFICATION_RESPONSE = "user_clarification_response" + FINAL_RESULT_MESSAGE = "final_result_message" + + +@dataclass(slots=True) +class WebsocketMessage: + """Generic WebSocket message wrapper.""" + type: WebsocketMessageType + message: Any + data: Any + + def to_dict(self) -> Dict[str, Any]: + """Convert the WebsocketMessage to a dictionary for JSON serialization.""" + return { + "type": self.type, + "data": self.data.to_dict() if hasattr(self.data, 'to_dict') else self.data, + "message": self.message.to_dict() if hasattr(self.message, 'to_dict') else self.message + } \ No newline at end of file diff --git a/src/backend/v3/models/orchestration_models.py b/src/backend/v3/models/orchestration_models.py index b4a893a2e..ef9f0759a 100644 --- a/src/backend/v3/models/orchestration_models.py +++ b/src/backend/v3/models/orchestration_models.py @@ -4,19 +4,6 @@ from semantic_kernel.kernel_pydantic import Field, KernelBaseModel -# This will be a dynamic dictionary and will depend on the loaded team definition -class AgentType(str, Enum): - """Enumeration of agent types.""" - - HUMAN = "Human_Agent" - HR = "Hr_Agent" - MARKETING = "Marketing_Agent" - PROCUREMENT = "Procurement_Agent" - PRODUCT = "Product_Agent" - GENERIC = "Generic_Agent" - TECH_SUPPORT = "Tech_Support_Agent" - GROUP_CHAT_MANAGER = "Group_Chat_Manager" - PLANNER = "Planner_Agent" # Add other agents as needed diff --git a/src/backend/v3/orchestration/human_approval_manager.py b/src/backend/v3/orchestration/human_approval_manager.py index 74bdaf13a..e54483a9c 100644 --- a/src/backend/v3/orchestration/human_approval_manager.py +++ b/src/backend/v3/orchestration/human_approval_manager.py @@ -87,9 +87,9 @@ async def plan(self, magentic_context: MagenticContext) -> Any: # Send the approval request to the user's WebSocket # The user_id will be automatically retrieved from context await connection_config.send_status_update_async({ - "type": "plan_approval_request", + "type": messages.WebsocketMessageType.PLAN_APPROVAL_REQUEST, "data": approval_message - }) + }, user_id=current_user_id.get(), message_type=messages.WebsocketMessageType.PLAN_APPROVAL_REQUEST) # Wait for user approval approval_response = await self._wait_for_user_approval(approval_message.plan.id) @@ -100,9 +100,9 @@ async def plan(self, magentic_context: MagenticContext) -> Any: else: print("Plan execution cancelled by user") await connection_config.send_status_update_async({ - "type": "plan_approval_response", + "type": messages.WebsocketMessageType.PLAN_APPROVAL_RESPONSE, "data": approval_response - }) + }, user_id=current_user_id.get(), message_type=messages.WebsocketMessageType.PLAN_APPROVAL_RESPONSE) raise Exception("Plan execution cancelled by user") # return ChatMessageContent( # role="assistant", diff --git a/src/backend/v3/orchestration/orchestration_manager.py b/src/backend/v3/orchestration/orchestration_manager.py index f2157c6d0..026c754ec 100644 --- a/src/backend/v3/orchestration/orchestration_manager.py +++ b/src/backend/v3/orchestration/orchestration_manager.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. """ Orchestration manager to handle the orchestration logic. """ +import asyncio import contextvars import os import uuid @@ -17,6 +18,7 @@ AzureChatCompletion, OpenAIChatPromptExecutionSettings) from semantic_kernel.contents import (ChatMessageContent, StreamingChatMessageContent) +from v3.models.messages import WebsocketMessageType from v3.callbacks.response_handlers import (agent_response_callback, streaming_agent_response_callback) from v3.config.settings import (config, connection_config, current_user_id, @@ -134,13 +136,13 @@ async def run_orchestration(self, user_id, input_task) -> None: # Send final result via WebSocket await connection_config.send_status_update_async({ - "type": "final_result", + "type": WebsocketMessageType.FINAL_RESULT_MESSAGE, "data": { "content": str(value), "status": "completed", - "timestamp": str(uuid.uuid4()) # or use actual timestamp + "timestamp": asyncio.get_event_loop().time() } - }, user_id) + }, user_id, message_type=WebsocketMessageType.FINAL_RESULT_MESSAGE) print(f"Final result sent via WebSocket to user {user_id}") except Exception as e: print(f"Error: {e}") diff --git a/src/frontend/src/api/apiService.tsx b/src/frontend/src/api/apiService.tsx index edd4142c6..75fb2e80b 100644 --- a/src/frontend/src/api/apiService.tsx +++ b/src/frontend/src/api/apiService.tsx @@ -16,17 +16,13 @@ import { // Constants for endpoints const API_ENDPOINTS = { - INPUT_TASK: '/input_task', PROCESS_REQUEST: '/v3/process_request', - PLANS: '/plans', - STEPS: '/steps', - HUMAN_FEEDBACK: '/human_feedback', - APPROVE_STEPS: '/approve_step_or_steps', - HUMAN_CLARIFICATION: '/human_clarification_on_plan', - AGENT_MESSAGES: '/agent_messages', - MESSAGES: '/messages', - USER_BROWSER_LANGUAGE: '/user_browser_language', - PLAN_APPROVAL: '/v3/plan_approval' + PLANS: '/v3/plans', + PLAN: '/v3/plan', + PLAN_APPROVAL: '/v3/plan_approval', + HUMAN_CLARIFICATION: '/v3/user_clarification', + USER_BROWSER_LANGUAGE: '/user_browser_language' + }; // Simple cache implementation @@ -105,14 +101,6 @@ export class APIService { private _cache = new APICache(); private _requestTracker = new RequestTracker(); - /** - * Submit a new input task to generate a plan - * @param inputTask The task description and optional session ID - * @returns Promise with the response containing session and plan IDs - */ - async submitInputTask(inputTask: InputTask): Promise { - return apiClient.post(API_ENDPOINTS.INPUT_TASK, inputTask); - } /** * Create a new plan with RAI validation @@ -163,7 +151,7 @@ export class APIService { const params = { plan_id: planId }; const fetcher = async () => { - const data = await apiClient.get(API_ENDPOINTS.PLANS, { params }); + const data = await apiClient.get(API_ENDPOINTS.PLAN, { params }); // The API returns an array, but with plan_id filter it should have only one item if (!data) { @@ -225,29 +213,6 @@ export class APIService { return fetcher(); } - /** - * Get steps for a specific plan - * @param planId Plan ID - * @param useCache Whether to use cached data or force fresh fetch - * @returns Promise with array of steps - */ - async getSteps(planId: string, useCache = true): Promise { - const cacheKey = `steps_${planId}`; - - const fetcher = async () => { - const data = await apiClient.get(`${API_ENDPOINTS.STEPS}/${planId}`); - if (useCache) { - this._cache.set(cacheKey, data, 30000); // Cache for 30 seconds - } - return data; - }; - - if (useCache) { - return this._requestTracker.trackRequest(cacheKey, fetcher); - } - - return fetcher(); - } /** * Approve a plan for execution @@ -347,68 +312,6 @@ export class APIService { return response; } - /** - * Get agent messages for a session - * @param sessionId Session ID - * @param useCache Whether to use cached data or force fresh fetch - * @returns Promise with array of agent messages - */ - async getAgentMessages(sessionId: string, useCache = true): Promise { - const cacheKey = `agent_messages_${sessionId}`; - - const fetcher = async () => { - const data = await apiClient.get(`${API_ENDPOINTS.AGENT_MESSAGES}/${sessionId}`); - if (useCache) { - this._cache.set(cacheKey, data, 30000); // Cache for 30 seconds - } - return data; - }; - - if (useCache) { - return this._requestTracker.trackRequest(cacheKey, fetcher); - } - - return fetcher(); - } - - - - - /** - * Delete all messages - * @returns Promise with response object - */ - async deleteAllMessages(): Promise<{ status: string }> { - const response = await apiClient.delete(API_ENDPOINTS.MESSAGES); - - // Clear all cached data - this._cache.clear(); - - return response; - } - - /** - * Get all messages - * @param useCache Whether to use cached data or force fresh fetch - * @returns Promise with array of messages - */ - async getAllMessages(useCache = true): Promise { - const cacheKey = 'all_messages'; - - const fetcher = async () => { - const data = await apiClient.get(API_ENDPOINTS.MESSAGES); - if (useCache) { - this._cache.set(cacheKey, data, 30000); // Cache for 30 seconds - } - return data; - }; - - if (useCache) { - return this._requestTracker.trackRequest(cacheKey, fetcher); - } - - return fetcher(); - } /** * Clear all cached data diff --git a/src/frontend/src/services/TaskService.tsx b/src/frontend/src/services/TaskService.tsx index 0044b5050..36ff32ea3 100644 --- a/src/frontend/src/services/TaskService.tsx +++ b/src/frontend/src/services/TaskService.tsx @@ -167,35 +167,6 @@ export class TaskService { return cleanedText; } - /** - * Submit an input task to create a new plan - * @param description Task description - * @returns Promise with the response containing session and plan IDs - */ - static async submitInputTask( - description: string - ): Promise { - const sessionId = this.generateSessionId(); - - const inputTask: InputTask = { - session_id: sessionId, - description: description, - }; - - try { - return await apiService.submitInputTask(inputTask); - } catch (error: any) { - // You can customize this logic as needed - let message = "Failed to create task."; - if (error?.response?.data?.message) { - message = error.response.data.message; - } else if (error?.message) { - message = error.message?.detail ? error.message.detail : error.message; - } - // Throw a new error with a user-friendly message - throw new Error(message); - } - } /** * Create a new plan with RAI validation