diff --git a/data/agent_teams/new 29.txt b/data/agent_teams/new 29.txt deleted file mode 100644 index cef35833e..000000000 --- a/data/agent_teams/new 29.txt +++ /dev/null @@ -1,48 +0,0 @@ -Tasks: - -Done: Make 3 teams upload work to cosmos (HR, Marketing, Retail). We will load this Cosmos data on deploy as default teams. -Done: - call "/socket/{process_id}" (start_comms) to setup websocket -Done: Make sure that a team is always selected - start with the hr.json team - -call init_team API for the currently loaded team on App start - -Spinner / team-loading should display until this call returns (user should not be able to input tasks) - - say something like "team loading" with spinner -FE: send unload current team API and call init team for team switch - sending select_team(new team id) - spin while waiting for return of API -BE: unload old team - load new team - return status - -BE: For Francia - implement get_plans to fill in history from cosmos - -BE: Create a teams container in Cosmos and move all loaded team definitions there - -Implement saving of plan to cosmos -> history in... - -================ Request submit flow ====================== -on request submission call "/process_request" (process_request) -This will return immediately - move to other page and display spinner -> "creating plan" -Socket will start receiving messages -> -Stream plan output into main window - -Will receive the PlanApprovalRequest message - Enable accept / reject UI -Send PlanApprovalResponse message when user answers - -If not approved - BE: plan will cancel on backend - FE: - enable input again for fresh request - Call input_request API on backend again (just like inputting any request) - -If approved: - Display plan steps in right pane if approved -============================================================= - -================== Message Streaming ======================== -Process socket message routing to display agent output - See message types in src\backend\v3\models\messages.py - for each message from agent - process stream then rollup - -On FinalResultMessage - display final result with all agent output in rollups by agent above -============================================================== - - diff --git a/src/backend/v3/api/router.py b/src/backend/v3/api/router.py index 708700599..97153486d 100644 --- a/src/backend/v3/api/router.py +++ b/src/backend/v3/api/router.py @@ -19,7 +19,8 @@ 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 connection_config, current_user_id, team_config +from v3.config.settings import (connection_config, current_user_id, + orchestration_config, team_config) from v3.orchestration.orchestration_manager import OrchestrationManager router = APIRouter() @@ -295,8 +296,35 @@ async def run_with_context(): ) raise HTTPException(status_code=400, detail=f"Error starting request: {e}") from e -@app_v3.post("/api/human_feedback") -async def human_feedback_endpoint(human_feedback: messages.HumanFeedback, request: Request): +@app_v3.post("/plan_approval") +async def plan_approval(human_feedback: messages.PlanApprovalResponse, request: Request): + """ Endpoint to receive plan approval or rejection from the user. """ + 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" + ) + # Set the approval in the orchestration config + if user_id and human_feedback.plan_id: + if orchestration_config and human_feedback.plan_id in orchestration_config.approvals: + orchestration_config.approvals[human_feedback.plan_id] = human_feedback.approved + track_event_if_configured( + "PlanApprovalReceived", + { + "plan_id": human_feedback.plan_id, + "approved": human_feedback.approved, + "user_id": user_id, + "feedback": human_feedback.feedback + }, + ) + return {"status": "approval recorded"} + else: + logging.warning(f"No orchestration or plan found for plan_id: {human_feedback.plan_id}") + raise HTTPException(status_code=404, detail="No active plan found for approval") + +@app_v3.post("/user_clarification") +async def user_clarification(human_feedback: messages.UserClarificationResponse, request: Request): pass diff --git a/src/backend/v3/magentic_agents/proxy_agent.py b/src/backend/v3/magentic_agents/proxy_agent.py index d6feab062..c1ad68285 100644 --- a/src/backend/v3/magentic_agents/proxy_agent.py +++ b/src/backend/v3/magentic_agents/proxy_agent.py @@ -6,7 +6,6 @@ from collections.abc import AsyncIterable from typing import AsyncIterator, Optional -import v3.models.messages as agent_messages from pydantic import Field from semantic_kernel.agents import ( # pylint: disable=no-name-in-module AgentResponseItem, AgentThread) @@ -22,6 +21,8 @@ from v3.callbacks.response_handlers import (agent_response_callback, streaming_agent_response_callback) from v3.config.settings import current_user_id +from v3.models.messages import (UserClarificationRequest, + UserClarificationResponse) class DummyAgentThread(AgentThread): @@ -145,6 +146,7 @@ async def invoke(self, message: str,*, thread: AgentThread | None = None,**kwarg ) # Send clarification request via response handlers clarification_request = f"I need clarification about: {message}" + clarification_message = self._create_message_content(clarification_request, thread.id) await self._trigger_response_callbacks(clarification_message) diff --git a/src/backend/v3/models/messages.py b/src/backend/v3/models/messages.py index 687ceb4d9..f2cb058fc 100644 --- a/src/backend/v3/models/messages.py +++ b/src/backend/v3/models/messages.py @@ -43,24 +43,26 @@ class PlanApprovalRequest: """Request for plan approval from the frontend.""" plan: MPlan status: PlanStatus - context: dict | None = None @dataclass(slots=True) class PlanApprovalResponse: """Response for plan approval from the frontend.""" + plan_id: str approved: bool feedback: str | None = None @dataclass(slots=True) class ReplanApprovalRequest: """Request for replan approval from the frontend.""" + new_plan: MPlan reason: str context: dict | None = None @dataclass(slots=True) class ReplanApprovalResponse: """Response for replan approval from the frontend.""" + plan_id: str approved: bool feedback: str | None = None @@ -73,8 +75,8 @@ class UserClarificationRequest: @dataclass(slots=True) class UserClarificationResponse: """Response for user clarification from the frontend.""" - def __init__(self, answer: str): - self.answer = answer + plan_id: str | None + answer: str = "" @dataclass(slots=True) class FinalResultMessage: @@ -111,12 +113,4 @@ class ApprovalRequest(KernelBaseModel): session_id: str user_id: str action: str - agent_name: str - -@dataclass(slots=True) -class HumanClarification(KernelBaseModel): - """Message containing human clarification on a plan.""" - - plan_id: str - session_id: str - human_clarification: str \ No newline at end of file + agent_name: str \ No newline at end of file diff --git a/src/backend/v3/models/models.py b/src/backend/v3/models/models.py index e62f3c686..d3423a956 100644 --- a/src/backend/v3/models/models.py +++ b/src/backend/v3/models/models.py @@ -27,32 +27,15 @@ def agent(self): def agent(self, value): self._agent = value if value is not None else "" + class MPlan(BaseModel): + """model of a plan""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) - session_id: Optional[str] = None - team_id: Optional[str] = None - user_id: Optional[str] = None + user_id: str = "" + team_id: str = "" + plan_id: str = "" overall_status: PlanStatus = PlanStatus.CREATED - progress: int = 0 # 0-100 percentage - current_step: Optional[str] = None - result: Optional[str] = None - error_message: Optional[str] = None - created_at: datetime = Field(datetime.now(timezone.utc)) - updated_at: datetime = Field(datetime.now(timezone.utc)) - estimated_completion: Optional[datetime] = None - user_request: Optional[str] = None + user_request: str = "" team: List[str] = [] - facts: Optional[str] = None - steps: List[MStep] = Field(default_factory=list) - -# class MPlan(BaseModel): -# """model of a plan""" -# session_id: str = "" -# user_id: str = "" -# team_id: str = "" -# plan_id: str = "" -# user_request: str = "" -# team: List[str] = [] -# facts: str = "" -# steps: List[MStep] = [] - + facts: str = "" + steps: List[MStep] = [] \ No newline at end of file diff --git a/src/backend/v3/orchestration/human_approval_manager.py b/src/backend/v3/orchestration/human_approval_manager.py index 2e358fe51..65e68eda9 100644 --- a/src/backend/v3/orchestration/human_approval_manager.py +++ b/src/backend/v3/orchestration/human_approval_manager.py @@ -3,6 +3,7 @@ Extends StandardMagenticManager to add approval gates before plan execution. """ +import asyncio import re from typing import Any, List, Optional @@ -13,7 +14,8 @@ from semantic_kernel.agents.orchestration.prompts._magentic_prompts import \ ORCHESTRATOR_TASK_LEDGER_FACTS_PROMPT from semantic_kernel.contents import ChatMessageContent -from v3.config.settings import connection_config, current_user_id +from v3.config.settings import (connection_config, current_user_id, + orchestration_config) from v3.models.models import MPlan, MStep @@ -104,18 +106,23 @@ async def plan(self, magentic_context: MagenticContext) -> Any: # ) - async def _wait_for_user_approval(self) -> Optional[messages.PlanApprovalResponse]: + async def _wait_for_user_approval(self, plan_id: Optional[str] = None) -> Optional[messages.PlanApprovalResponse]: # plan_id will not be optional in future """Wait for user approval response.""" - user_id = current_user_id.get() # Temporarily use console input for approval - will switch to WebSocket or API in future - response = input("\nApprove this execution plan? [y/n]: ").strip().lower() - if response in ['y', 'yes']: - return messages.PlanApprovalResponse(approved=True) - elif response in ['n', 'no']: - return messages.PlanApprovalResponse(approved=False) - else: - print("Invalid input. Please enter 'y' for yes or 'n' for no.") - return await self._wait_for_user_approval() + # response = input("\nApprove this execution plan? [y/n]: ").strip().lower() + # if response in ['y', 'yes']: + # return messages.PlanApprovalResponse(approved=True, plan_id=plan_id if plan_id else "input") + # elif response in ['n', 'no']: + # return messages.PlanApprovalResponse(approved=False, plan_id=plan_id if plan_id else "input") + # else: + # print("Invalid input. Please enter 'y' for yes or 'n' for no.") + # return await self._wait_for_user_approval() + # In future, implement actual waiting for WebSocket or API response here + if plan_id not in orchestration_config.approvals: + orchestration_config.approvals[plan_id] = None + while orchestration_config.approvals[plan_id] is None: + await asyncio.sleep(0.2) + return messages.PlanApprovalResponse(approved=orchestration_config.approvals[plan_id], plan_id=plan_id) async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessageContent: diff --git a/src/mcp_server/config/settings.py b/src/mcp_server/config/settings.py index 96441f1f8..58e96634b 100644 --- a/src/mcp_server/config/settings.py +++ b/src/mcp_server/config/settings.py @@ -4,32 +4,38 @@ import os from typing import Optional -from pydantic import BaseModel, Field + +from pydantic import BaseModel, ConfigDict, Field from pydantic_settings import BaseSettings class MCPServerConfig(BaseSettings): """MCP Server configuration.""" + + model_config = ConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore" # This will ignore extra environment variables + ) # Server settings - host: str = Field(default="0.0.0.0", env="MCP_HOST") - port: int = Field(default=9000, env="MCP_PORT") - debug: bool = Field(default=False, env="MCP_DEBUG") + host: str = Field(default="0.0.0.0") + port: int = Field(default=9000) + debug: bool = Field(default=False) # Authentication settings - tenant_id: Optional[str] = Field(default=None, env="AZURE_TENANT_ID") - client_id: Optional[str] = Field(default=None, env="AZURE_CLIENT_ID") - jwks_uri: Optional[str] = Field(default=None, env="AZURE_JWKS_URI") - issuer: Optional[str] = Field(default=None, env="AZURE_ISSUER") - audience: Optional[str] = Field(default=None, env="AZURE_AUDIENCE") + tenant_id: Optional[str] = Field(default=None) + client_id: Optional[str] = Field(default=None) + jwks_uri: Optional[str] = Field(default=None) + issuer: Optional[str] = Field(default=None) + audience: Optional[str] = Field(default=None) # MCP specific settings - server_name: str = Field(default="MACAE MCP Server", env="MCP_SERVER_NAME") - enable_auth: bool = Field(default=True, env="MCP_ENABLE_AUTH") - - class Config: - env_file = ".env" - env_file_encoding = "utf-8" + server_name: str = Field(default="MACAE MCP Server") + enable_auth: bool = Field(default=True) + + # Dataset path - added to handle the environment variable + dataset_path: str = Field(default="./datasets") # Global configuration instance