diff --git a/src/agents/common/agent.py b/src/agents/common/agent.py index 779cc209..0645da10 100644 --- a/src/agents/common/agent.py +++ b/src/agents/common/agent.py @@ -16,6 +16,7 @@ IS_LAST_STEP, MESSAGES, MY_TASK, + SUBTASKS, SUMMARIZATION, ) from agents.common.state import BaseAgentState, SubTaskStatus @@ -180,7 +181,7 @@ def _finalizer_node(self, state: BaseAgentState, config: RunnableConfig) -> Any: if state.my_task is not None: state.my_task.complete() # clean all agent messages to avoid populating the checkpoint with unnecessary messages. - return {MESSAGES: [state.agent_messages[-1]]} + return {MESSAGES: [state.agent_messages[-1]], SUBTASKS: state.subtasks} def _build_graph(self, state_class: type) -> Any: # Define a new graph diff --git a/src/agents/common/constants.py b/src/agents/common/constants.py index eceb889a..01915c00 100644 --- a/src/agents/common/constants.py +++ b/src/agents/common/constants.py @@ -1,7 +1,5 @@ PLANNER = "Planner" -COMMON = "Common" - SUMMARIZATION = "Summarization" FINALIZER = "Finalizer" @@ -42,8 +40,16 @@ K8S_AGENT = "KubernetesAgent" +K8S_AGENT_TASK_DESCRIPTION = "Fetching data from Kubernetes" + KYMA_AGENT = "KymaAgent" +KYMA_AGENT_TASK_DESCRIPTION = "Fetching data from Kyma" + +COMMON = "Common" + +COMMON_TASK_DESCRIPTION = "Answering general queries" + RESPONSE_CONVERTER = "ResponseConverter" NEW_YAML = "New" diff --git a/src/agents/common/state.py b/src/agents/common/state.py index 74f77f26..7f03e188 100644 --- a/src/agents/common/state.py +++ b/src/agents/common/state.py @@ -28,9 +28,12 @@ class SubTask(BaseModel): description: str = Field( description="user query with original wording for the assigned agent" ) + task_title: str = Field( + description="""Generate a title of 4 to 5 words, only use these: + 'Retrieving', 'Fetching', 'Extracting' or 'Checking'. Never use 'Creating'.""" + ) assigned_to: Literal[KYMA_AGENT, K8S_AGENT, COMMON] # type: ignore status: str = Field(default=SubTaskStatus.PENDING) - result: str | None = None def complete(self) -> None: """Update the result of the task.""" diff --git a/src/agents/graph.py b/src/agents/graph.py index 968695ed..249065f0 100644 --- a/src/agents/graph.py +++ b/src/agents/graph.py @@ -28,6 +28,7 @@ COMMON, MESSAGES, MESSAGES_SUMMARY, + SUBTASKS, SUMMARIZATION, ) from agents.common.data import Message @@ -174,6 +175,7 @@ async def _common_node(self, state: CompanionState) -> dict[str, Any]: name=COMMON, ) ], + SUBTASKS: state.subtasks, } except Exception as e: logger.error(f"Error in common node: {e}") @@ -183,7 +185,8 @@ async def _common_node(self, state: CompanionState) -> dict[str, Any]: content="Sorry, I am unable to process the request.", name=COMMON, ) - ] + ], + SUBTASKS: state.subtasks, } return { MESSAGES: [ @@ -191,7 +194,8 @@ async def _common_node(self, state: CompanionState) -> dict[str, Any]: content="All my subtasks are already completed.", name=COMMON, ) - ] + ], + SUBTASKS: state.subtasks, } def _build_graph(self) -> CompiledGraph: diff --git a/src/agents/supervisor/agent.py b/src/agents/supervisor/agent.py index 640528ad..d765a78e 100644 --- a/src/agents/supervisor/agent.py +++ b/src/agents/supervisor/agent.py @@ -186,6 +186,7 @@ async def _plan(self, state: SupervisorState) -> dict[str, Any]: if plan.response: return create_node_output( message=AIMessage(content=plan.response, name=PLANNER), + subtasks=[], # empty subtask to make the companion response consistent next=END, ) @@ -194,6 +195,7 @@ async def _plan(self, state: SupervisorState) -> dict[str, Any]: raise SubtasksMissingError(str(state.messages[-1].content)) # return the plan with the subtasks to be dispatched by the Router return create_node_output( + message=AIMessage(content="", name=PLANNER), next=ROUTER, subtasks=plan.subtasks, ) diff --git a/src/agents/supervisor/prompts.py b/src/agents/supervisor/prompts.py index 50bee515..0f535562 100644 --- a/src/agents/supervisor/prompts.py +++ b/src/agents/supervisor/prompts.py @@ -64,24 +64,22 @@ - **Be Context-Aware**: Always consider the broader context of the conversation to ensure responses are relevant and accurate. # SAMPLE QUERIES AND RESPONSES: -- Kyma or Kubernetes related queries: - Query: "What is Kyma serverless? what is the status of my cluster?" +Query: "What is Kyma serverless? what is the status of my cluster?" - "response": None, - "subtasks": [ - ("description": "What is Kyma serverless?","assigned_to": "KymaAgent") , - ("description": "what is the status of my cluster?","assigned_to": "KubernetesAgent") - ] - + "response": None, + "subtasks": [ + ("description": "What is Kyma serverless?","assigned_to": "KymaAgent" , "task_title" : "Fetching info about Kyma serverless") , + ("description": "what is the status of my cluster?","assigned_to": "KubernetesAgent", "task_title" : "Checking status of cluster")] + + + Query: "What is kubernetes and Create a hello world app and deploy it with Kyma?" -- Common and Kyma related queries: - Query: "parse the json script in python and deploy it with Kyma?" - "response": None, "subtasks": [ - ("description": "parse the json script in python", "assigned_to": "Common"), - ("description": "deploy the app with Kyma","assigned_to": "KymaAgent") - ] + ("description": "What is kubernetes", "assigned_to": "KubernetesAgent"), + ("description": "Create a hello world app", "assigned_to": "Common"), + ("description": "deploy the app with Kyma","assigned_to": "KymaAgent") + ] - General query and can be answered directly: Query: "Where is Nils river located?" diff --git a/src/routers/conversations.py b/src/routers/conversations.py index df21b45b..0561a676 100644 --- a/src/routers/conversations.py +++ b/src/routers/conversations.py @@ -177,10 +177,12 @@ async def messages( return StreamingResponse( ( - prepare_chunk_response(chunk) + b"\n" + chunk_response + b"\n" async for chunk in conversation_service.handle_request( conversation_id, message, k8s_client ) + for chunk_response in (prepare_chunk_response(chunk),) + if chunk_response is not None ), media_type="text/event-stream", ) diff --git a/src/utils/response.py b/src/utils/response.py index f868fc9d..97c0991d 100644 --- a/src/utils/response.py +++ b/src/utils/response.py @@ -1,13 +1,35 @@ import json from typing import Any -from agents.common.constants import PLANNER +from agents.common.constants import ( + FINALIZER, + NEXT, + PLANNER, + SUMMARIZATION, +) +from agents.common.state import SubTaskStatus from agents.supervisor.agent import SUPERVISOR from utils.logging import get_logger logger = get_logger(__name__) +def reformat_subtasks(subtasks: list[dict[Any, Any]]) -> list[dict[str, Any]]: + """Reformat subtasks list for companion response""" + tasks = [] + if subtasks: + for i, subtask in enumerate(subtasks): + task = { + "task_id": i, + "task_name": subtask["task_title"], + "status": subtask["status"], + "agent": subtask["assigned_to"], + } + + tasks.append(task) + return tasks + + def process_response(data: dict[str, Any], agent: str) -> dict[str, Any]: """Process agent data and return the last message only.""" agent_data = data[agent] @@ -16,19 +38,30 @@ def process_response(data: dict[str, Any], agent: str) -> dict[str, Any]: return {"agent": agent, "error": agent_data["error"]} answer = {} + if "messages" in agent_data and agent_data["messages"]: answer["content"] = agent_data["messages"][-1].get("content") - if agent == PLANNER: - answer["subtasks"] = agent_data.get("subtasks") + answer["tasks"] = reformat_subtasks(agent_data.get("subtasks")) if agent == SUPERVISOR: - answer["next"] = agent_data.get("next") + answer[NEXT] = agent_data.get(NEXT) + else: + if agent_data.get("subtasks"): + pending_subtask = [ + subtask["assigned_to"] + for subtask in agent_data.get("subtasks") + if subtask["status"] == SubTaskStatus.PENDING + ] + if pending_subtask: + answer[NEXT] = pending_subtask[0] + else: + answer[NEXT] = FINALIZER return {"agent": agent, "answer": answer} -def prepare_chunk_response(chunk: bytes) -> bytes: +def prepare_chunk_response(chunk: bytes) -> bytes | None: """Converts and prepares a final chunk response.""" try: data = json.loads(chunk) @@ -39,12 +72,24 @@ def prepare_chunk_response(chunk: bytes) -> bytes: ).encode() agent = next(iter(data.keys()), None) + + # skip summarization node + if agent == SUMMARIZATION: + return None + if not agent: logger.error(f"Agent {agent} is not found in the json data") return json.dumps( {"event": "unknown", "data": {"error": "No agent found"}} ).encode() + agent_data = data[agent] + if agent_data.get("messages"): + last_agent = agent_data["messages"][-1].get("name") + # skip all intermediate supervisor response + if agent == SUPERVISOR and last_agent != PLANNER and last_agent != FINALIZER: + return None + new_data = process_response(data, agent) return json.dumps( diff --git a/tests/integration/agents/kyma/test_kyma_agent.py b/tests/integration/agents/kyma/test_kyma_agent.py index e4876beb..342a9abf 100644 --- a/tests/integration/agents/kyma/test_kyma_agent.py +++ b/tests/integration/agents/kyma/test_kyma_agent.py @@ -127,11 +127,14 @@ def kyma_agent(app_models): subtasks=[ { "description": "What is wrong with ApiRule?", + "task_title": "What is wrong with ApiRule?", "assigned_to": "KymaAgent", } ], my_task=SubTask( - description="What is wrong with API rule?", assigned_to="KymaAgent" + description="What is wrong with API rule?", + task_title="What is wrong with API rule?", + assigned_to="KymaAgent", ), k8s_client=Mock(spec_set=IK8sClient), is_last_step=False, @@ -156,11 +159,14 @@ def kyma_agent(app_models): subtasks=[ { "description": "What is wrong with ApiRule?", + "task_title": "What is wrong with ApiRule?", "assigned_to": "KymaAgent", } ], my_task=SubTask( - description="What is wrong with api rule?", assigned_to="KymaAgent" + description="What is wrong with api rule?", + task_title="What is wrong with api rule?", + assigned_to="KymaAgent", ), k8s_client=Mock(spec_set=IK8sClient), # noqa is_last_step=False, @@ -204,11 +210,14 @@ def kyma_agent(app_models): subtasks=[ { "description": "What is wrong with ApiRule?", + "task_title": "What is wrong with ApiRule?", "assigned_to": "KymaAgent", } ], my_task=SubTask( - description="What is wrong with ApiRule?", assigned_to="KymaAgent" + description="What is wrong with ApiRule?", + task_title="What is wrong with ApiRule?", + assigned_to="KymaAgent", ), k8s_client=Mock(spec_set=IK8sClient), # noqa is_last_step=False, @@ -268,11 +277,13 @@ def kyma_agent(app_models): subtasks=[ { "description": "What is wrong with Function 'func1' in namespace 'kyma-app-serverless-syntax-err' with api version 'serverless.kyma-project.io/v1alpha2'?", + "task_title": "What is wrong with Function 'func1' in namespace 'kyma-app-serverless-syntax-err' with api version 'serverless.kyma-project.io/v1alpha2'?", "assigned_to": "KymaAgent", } ], my_task=SubTask( description="What is wrong with Function 'func1' in namespace 'kyma-app-serverless-syntax-err' with api version 'serverless.kyma-project.io/v1alpha2'?", + task_title="What is wrong with Function 'func1' in namespace 'kyma-app-serverless-syntax-err' with api version 'serverless.kyma-project.io/v1alpha2'?", assigned_to="KymaAgent", ), k8s_client=Mock(spec_set=IK8sClient), # noqa @@ -330,11 +341,13 @@ def kyma_agent(app_models): subtasks=[ { "description": "Why the pod of the serverless Function is not ready?", + "task_title": "Why the pod of the serverless Function is not ready?", "assigned_to": "KymaAgent", } ], my_task=SubTask( description="Why the pod of the serverless Function is not ready?", + task_title="Why the pod of the serverless Function is not ready?", assigned_to="KymaAgent", ), k8s_client=Mock(spec_set=IK8sClient), # noqa @@ -376,11 +389,13 @@ def kyma_agent(app_models): subtasks=[ { "description": "what are the BTP Operator features?", + "task_title": "what are the BTP Operator features?", "assigned_to": "KymaAgent", } ], my_task=SubTask( description="What are the BTP Operator features?", + task_title="What are the BTP Operator features?", assigned_to="KymaAgent", ), k8s_client=Mock(spec_set=IK8sClient), # noqa @@ -420,11 +435,13 @@ def kyma_agent(app_models): subtasks=[ { "description": "what are the BTP Operator features?", + "task_title": "what are the BTP Operator features?", "assigned_to": "KymaAgent", } ], my_task=SubTask( description="What are the BTP Operator features?", + task_title="What are the BTP Operator features?", assigned_to="KymaAgent", ), k8s_client=Mock(spec_set=IK8sClient), # noqa diff --git a/tests/integration/agents/supervisor/test_planner.py b/tests/integration/agents/supervisor/test_planner.py index 8fd98eb9..9de7eebb 100644 --- a/tests/integration/agents/supervisor/test_planner.py +++ b/tests/integration/agents/supervisor/test_planner.py @@ -46,7 +46,7 @@ def planner_correctness_metric(evaluator_model): ), HumanMessage(content="What is the capital of Germany?"), ], - '{"subtasks": null, "response": "Berlin" }', + '{"subtasks":null,"response":"The capital of Germany is Berlin."}', True, ), ( @@ -88,7 +88,7 @@ def planner_correctness_metric(evaluator_model): ), HumanMessage(content="why the pod is failing?"), ], - "{'subtasks': [{ 'description': 'why the pod is failing?', 'assigned_to': 'KubernetesAgent' ,'status' : 'pending'}] , 'response': null}", + "{'subtasks': None , 'response': 'pods is failing due to a context cancellation.'}", False, ), ( @@ -110,10 +110,10 @@ def planner_correctness_metric(evaluator_model): content="The user query is related to: " "{'resource_api_version': 'v1', 'resource_namespace': 'nginx-oom'}" ), - HumanMessage(content="What is Kubernetes? Explain Kyma function"), + HumanMessage(content="What is Kubernetes and Explain Kyma function"), ], - '{"subtasks": [{ "description": "What is Kubernetes?", "assigned_to": "KubernetesAgent","status" : "pending"},' - '{"description": "Explain Kyma function", "assigned_to": "KymaAgent","status" : "pending"}] , "response": null}', + '{"subtasks": [{ "description": "What is Kubernetes", "assigned_to": "KubernetesAgent","status" : "pending"},' + '{"description": "Explain Kyma function", "assigned_to": "KymaAgent","status" : "pending"}] , "response": null}', False, ), ( diff --git a/tests/integration/agents/supervisor/test_router.py b/tests/integration/agents/supervisor/test_router.py index 3d7ded28..9a4bbb7a 100644 --- a/tests/integration/agents/supervisor/test_router.py +++ b/tests/integration/agents/supervisor/test_router.py @@ -24,7 +24,12 @@ ) ], [ - SubTask(description="Task 1", assigned_to=K8S_AGENT, status="pending"), + SubTask( + description="Task 1", + task_title="Task 1", + assigned_to=K8S_AGENT, + status="pending", + ), ], K8S_AGENT, ), @@ -37,7 +42,12 @@ ) ], [ - SubTask(description="Task 1", assigned_to=KYMA_AGENT, status="pending"), + SubTask( + description="Task 1", + task_title="Task 1", + assigned_to=KYMA_AGENT, + status="pending", + ), ], KYMA_AGENT, ), @@ -50,7 +60,12 @@ ) ], [ - SubTask(description="Task 1", assigned_to=COMMON, status="pending"), + SubTask( + description="Task 1", + task_title="Task 1", + assigned_to=COMMON, + status="pending", + ), ], COMMON, ), @@ -67,8 +82,18 @@ ) ], [ - SubTask(description="Task 1", assigned_to=K8S_AGENT, status="pending"), - SubTask(description="Task 2", assigned_to=KYMA_AGENT, status="pending"), + SubTask( + description="Task 1", + task_title="Task 1", + assigned_to=K8S_AGENT, + status="pending", + ), + SubTask( + description="Task 2", + task_title="Task 2", + assigned_to=KYMA_AGENT, + status="pending", + ), ], K8S_AGENT, ), @@ -81,8 +106,18 @@ ) ], [ - SubTask(description="Task 1", assigned_to=KYMA_AGENT, status="pending"), - SubTask(description="Task 2", assigned_to=K8S_AGENT, status="pending"), + SubTask( + description="Task 1", + task_title="Task 1", + assigned_to=KYMA_AGENT, + status="pending", + ), + SubTask( + description="Task 2", + task_title="Task 2", + assigned_to=K8S_AGENT, + status="pending", + ), ], KYMA_AGENT, ), @@ -95,8 +130,18 @@ ) ], [ - SubTask(description="Task 1", assigned_to=COMMON, status="pending"), - SubTask(description="Task 2", assigned_to=KYMA_AGENT, status="pending"), + SubTask( + description="Task 1", + task_title="Task 1", + assigned_to=COMMON, + status="pending", + ), + SubTask( + description="Task 2", + task_title="Task 2", + assigned_to=KYMA_AGENT, + status="pending", + ), ], COMMON, ), @@ -114,9 +159,17 @@ ], [ SubTask( - description="Task 1", assigned_to=KYMA_AGENT, status="completed" + description="Task 1", + task_title="Task 1", + assigned_to=KYMA_AGENT, + status="completed", + ), + SubTask( + description="Task 2", + task_title="Task 2", + assigned_to=K8S_AGENT, + status="pending", ), - SubTask(description="Task 2", assigned_to=K8S_AGENT, status="pending"), ], K8S_AGENT, ), @@ -130,9 +183,17 @@ ], [ SubTask( - description="Task 1", assigned_to=K8S_AGENT, status="completed" + description="Task 1", + task_title="Task 1", + assigned_to=K8S_AGENT, + status="completed", + ), + SubTask( + description="Task 2", + task_title="Task 2", + assigned_to=KYMA_AGENT, + status="pending", ), - SubTask(description="Task 2", assigned_to=KYMA_AGENT, status="pending"), ], KYMA_AGENT, ), @@ -146,9 +207,17 @@ ], [ SubTask( - description="Task 1", assigned_to=KYMA_AGENT, status="completed" + description="Task 1", + task_title="Task 1", + assigned_to=KYMA_AGENT, + status="completed", + ), + SubTask( + description="Task 2", + task_title="Task 2", + assigned_to=COMMON, + status="pending", ), - SubTask(description="Task 2", assigned_to=COMMON, status="pending"), ], COMMON, ), diff --git a/tests/unit/agents/common/test_agent.py b/tests/unit/agents/common/test_agent.py index e69347b4..a4f371b6 100644 --- a/tests/unit/agents/common/test_agent.py +++ b/tests/unit/agents/common/test_agent.py @@ -146,7 +146,9 @@ def test_create_chain(self, mock_models): subtasks=[], k8s_client=Mock(spec=IK8sClient), my_task=SubTask( - description="test 1", assigned_to="KubernetesAgent" + description="test 1", + task_title="test", + assigned_to="KubernetesAgent", ), ), { @@ -168,7 +170,9 @@ def test_create_chain(self, mock_models): subtasks=[], k8s_client=Mock(spec=IK8sClient), my_task=SubTask( - description="test 2", assigned_to="KubernetesAgent" + description="test 2", + task_title="test", + assigned_to="KubernetesAgent", ), ), { @@ -246,14 +250,24 @@ def test_build_graph(self, mock_models): messages=[], agent_messages=[], subtasks=[ - SubTask(description="test", assigned_to="KubernetesAgent"), - SubTask(description="test", assigned_to="KubernetesAgent"), + SubTask( + description="test", + task_title="test", + assigned_to="KubernetesAgent", + ), + SubTask( + description="test", + task_title="test", + assigned_to="KubernetesAgent", + ), ], k8s_client=Mock(spec=IK8sClient), ), { "my_task": SubTask( - description="test", assigned_to="KubernetesAgent" + description="test", + task_title="test", + assigned_to="KubernetesAgent", ), }, ), @@ -265,7 +279,11 @@ def test_build_graph(self, mock_models): messages=[], agent_messages=[], subtasks=[ - SubTask(description="test", assigned_to="KymaAgent"), + SubTask( + description="test", + task_title="test", + assigned_to="KymaAgent", + ), ], k8s_client=Mock(spec=IK8sClient), ), @@ -287,14 +305,20 @@ def test_build_graph(self, mock_models): messages=[], agent_messages=[], subtasks=[ - SubTask(description="test", assigned_to="KymaAgent"), + SubTask( + description="test", + task_title="test", + assigned_to="KymaAgent", + ), SubTask( description="test1", + task_title="test1", assigned_to="KubernetesAgent", status=SubTaskStatus.COMPLETED, ), SubTask( description="test2", + task_title="test2", assigned_to="KubernetesAgent", status=SubTaskStatus.COMPLETED, ), @@ -329,7 +353,9 @@ def test_subtask_selector_node( None, TestAgentState( my_task=SubTask( - description="test task 1", assigned_to="KubernetesAgent" + description="test task 1", + task_title="test task 1", + assigned_to="KubernetesAgent", ), is_last_step=False, messages=[AIMessage(content="dummy message 1")], @@ -355,7 +381,9 @@ def test_subtask_selector_node( ValueError("This is a dummy exception from model."), TestAgentState( my_task=SubTask( - description="test task 1", assigned_to="KubernetesAgent" + description="test task 1", + task_title="test task 1", + assigned_to="KubernetesAgent", ), is_last_step=False, messages=[AIMessage(content="dummy message 1")], @@ -391,7 +419,9 @@ def test_subtask_selector_node( None, TestAgentState( my_task=SubTask( - description="test task 1", assigned_to="KubernetesAgent" + description="test task 1", + task_title="test task 1", + assigned_to="KubernetesAgent", ), is_last_step=True, messages=[AIMessage(content="dummy message 1")], @@ -448,6 +478,7 @@ async def test_model_node( TestAgentState( my_task=SubTask( description="test task 1", + task_title="test task 1", assigned_to="KubernetesAgent", status=SubTaskStatus.PENDING, ), @@ -483,6 +514,7 @@ async def test_model_node( "messages": [ AIMessage(id="3", content="final answer"), ], + "subtasks": [], }, ), ], diff --git a/tests/unit/agents/common/test_state.py b/tests/unit/agents/common/test_state.py index 4f730582..50bc7f0b 100644 --- a/tests/unit/agents/common/test_state.py +++ b/tests/unit/agents/common/test_state.py @@ -19,9 +19,10 @@ class TestSubTask: # Test data for SubTask @pytest.mark.parametrize( - "description, assigned_to, status, result, expected_status", + "description, task_title,assigned_to, status, result, expected_status", [ ( + "Task 1", "Task 1", "KymaAgent", SubTaskStatus.PENDING, @@ -29,6 +30,7 @@ class TestSubTask: SubTaskStatus.COMPLETED, ), ( + "Task 2", "Task 2", "KymaAgent", SubTaskStatus.PENDING, @@ -37,9 +39,12 @@ class TestSubTask: ), ], ) - def test_complete(self, description, assigned_to, status, result, expected_status): + def test_complete( + self, description, task_title, assigned_to, status, result, expected_status + ): subtask = SubTask( description=description, + task_title=task_title, assigned_to=assigned_to, status=status, result=result, @@ -107,11 +112,13 @@ class TestAgentState: [ SubTask( description="Task 1", + task_title="Task 1", assigned_to="KymaAgent", status=SubTaskStatus.COMPLETED, ), SubTask( description="Task 2", + task_title="Task 2", assigned_to="KubernetesAgent", status=SubTaskStatus.COMPLETED, ), @@ -125,11 +132,13 @@ class TestAgentState: [ SubTask( description="Task 1", + task_title="Task 1", assigned_to="KymaAgent", status=SubTaskStatus.COMPLETED, ), SubTask( description="Task 2", + task_title="Task 2", assigned_to="KubernetesAgent", status=SubTaskStatus.PENDING, ), diff --git a/tests/unit/agents/common/test_utils.py b/tests/unit/agents/common/test_utils.py index fc71bd39..706f3e65 100644 --- a/tests/unit/agents/common/test_utils.py +++ b/tests/unit/agents/common/test_utils.py @@ -360,7 +360,7 @@ def test_filter_messages_default_parameter(): [ ( False, - SubTask(description="test", assigned_to=K8S_AGENT), + SubTask(description="test", task_title="test", assigned_to=K8S_AGENT), "agent", ), ( diff --git a/tests/unit/agents/supervisor/test_supervisor_agent.py b/tests/unit/agents/supervisor/test_supervisor_agent.py index f06d7a26..d0d27d61 100644 --- a/tests/unit/agents/supervisor/test_supervisor_agent.py +++ b/tests/unit/agents/supervisor/test_supervisor_agent.py @@ -38,10 +38,16 @@ def supervisor_agent(self, mock_models): ( [ SubTask( - description="Task 1", assigned_to=K8S_AGENT, status="pending" + description="Task 1", + task_title="Task 1", + assigned_to=K8S_AGENT, + status="pending", ), SubTask( - description="Task 2", assigned_to=KYMA_AGENT, status="pending" + description="Task 2", + task_title="Task 2", + assigned_to=KYMA_AGENT, + status="pending", ), ], [ @@ -51,10 +57,16 @@ def supervisor_agent(self, mock_models): K8S_AGENT, [ SubTask( - description="Task 1", assigned_to=K8S_AGENT, status="pending" + description="Task 1", + task_title="Task 1", + assigned_to=K8S_AGENT, + status="pending", ), SubTask( - description="Task 2", assigned_to=KYMA_AGENT, status="pending" + description="Task 2", + task_title="Task 2", + assigned_to=KYMA_AGENT, + status="pending", ), ], None, @@ -63,6 +75,7 @@ def supervisor_agent(self, mock_models): [ SubTask( description="Task 2", + task_title="Task 2", assigned_to=KYMA_AGENT, status="in_progress", ) @@ -72,6 +85,7 @@ def supervisor_agent(self, mock_models): [ SubTask( description="Task 2", + task_title="Task 2", assigned_to=KYMA_AGENT, status="in_progress", ) @@ -81,14 +95,20 @@ def supervisor_agent(self, mock_models): ( [ SubTask( - description="Task 3", assigned_to=K8S_AGENT, status="completed" + description="Task 3", + task_title="Task 3", + assigned_to=K8S_AGENT, + status="completed", ) ], [], FINALIZER, [ SubTask( - description="Task 3", assigned_to=K8S_AGENT, status="completed" + description="Task 3", + task_title="Task 3", + assigned_to=K8S_AGENT, + status="completed", ) ], None, @@ -210,19 +230,22 @@ async def test_agent_generate_final_response( ( "Plans multiple subtasks successfully", "How do I deploy a Kyma function?", - '{"response":null, "subtasks": [{"description": "Explain Kyma function deployment", "assigned_to": "KymaAgent" ,"status" : "pending"},' - '{"description": "Explain K8s deployment", "assigned_to": "KubernetesAgent","status" : "pending"}]}', + '{"response":null, "subtasks": [{"description": "Explain Kyma function deployment", "task_title": "Explain Kyma function deployment", "assigned_to": "KymaAgent" ,"status" : "pending"},' + '{"description": "Explain K8s deployment", "task_title": "Explain K8s deployment", "assigned_to": "KubernetesAgent","status" : "pending"}]}', { "subtasks": [ SubTask( description="Explain Kyma function deployment", + task_title="Explain Kyma function deployment", assigned_to=KYMA_AGENT, ), SubTask( - description="Explain K8s deployment", assigned_to=K8S_AGENT + description="Explain K8s deployment", + task_title="Explain K8s deployment", + assigned_to=K8S_AGENT, ), ], - "messages": [], + "messages": [AIMessage(content="", name="Planner")], "error": None, "next": ROUTER, }, @@ -231,15 +254,16 @@ async def test_agent_generate_final_response( ( "Plans a single subtask successfully", "What is a Kubernetes pod?", - '{"response":null, "subtasks": [{"description": "Explain Kubernetes pod concept", "assigned_to": "KubernetesAgent","status" : "pending"}]}', + '{"response":null, "subtasks": [{"description": "Explain Kubernetes pod concept","task_title": "Explain Kubernetes pod concept", "assigned_to": "KubernetesAgent","status" : "pending"}]}', { "subtasks": [ SubTask( description="Explain Kubernetes pod concept", + task_title="Explain Kubernetes pod concept", assigned_to="KubernetesAgent", ) ], - "messages": [], + "messages": [AIMessage(content="", name="Planner")], "error": None, "next": ROUTER, }, @@ -267,7 +291,7 @@ async def test_agent_generate_final_response( ) ], "next": END, - "subtasks": None, + "subtasks": [], }, None, ), diff --git a/tests/unit/agents/test_graph.py b/tests/unit/agents/test_graph.py index 47aa7800..554d343e 100644 --- a/tests/unit/agents/test_graph.py +++ b/tests/unit/agents/test_graph.py @@ -88,6 +88,7 @@ class TestCompanionGraph: description="Explain Python", assigned_to=COMMON, status="pending", + task_title="Explain Python", ) ], [HumanMessage(content="What is Java?")], @@ -95,9 +96,18 @@ class TestCompanionGraph: { "messages": [ AIMessage( - content="Python is a high-level programming language. " - "Java is a general-purpose programming language.", - name=COMMON, + content="Python is a high-level programming language. Java is a general-purpose programming language.", + additional_kwargs={}, + response_metadata={}, + name="Common", + ) + ], + "subtasks": [ + SubTask( + description="Explain Python", + task_title="Explain Python", + assigned_to="Common", + status="completed", ) ], }, @@ -108,11 +118,13 @@ class TestCompanionGraph: [ SubTask( description="Explain Python", + task_title="Explain Python", assigned_to=COMMON, status="pending", ), SubTask( description="Explain Java", + task_title="Explain Java", assigned_to=COMMON, status="pending", ), @@ -127,6 +139,20 @@ class TestCompanionGraph: name=COMMON, ) ], + "subtasks": [ + SubTask( + description="Explain Python", + task_title="Explain Python", + assigned_to="Common", + status="completed", + ), + SubTask( + description="Explain Java", + task_title="Explain Java", + assigned_to="Common", + status="pending", + ), + ], }, None, ), @@ -135,6 +161,7 @@ class TestCompanionGraph: [ SubTask( description="Explain Python", + task_title="Explain Python", assigned_to=COMMON, status="completed", ) @@ -148,6 +175,14 @@ class TestCompanionGraph: name=COMMON, ) ], + "subtasks": [ + SubTask( + description="Explain Python", + task_title="Explain Python", + assigned_to="Common", + status="completed", + ) + ], }, None, ), @@ -156,6 +191,7 @@ class TestCompanionGraph: [ SubTask( description="Explain Python", + task_title="Explain Python", assigned_to=COMMON, status="pending", ) @@ -168,7 +204,15 @@ class TestCompanionGraph: content="Sorry, I am unable to process the request.", name=COMMON, ) - ] + ], + "subtasks": [ + SubTask( + description="Explain Python", + task_title="Explain Python", + assigned_to="Common", + status="pending", + ) + ], }, "Error in common node: Test error", ), diff --git a/tests/unit/routers/test_conversations.py b/tests/unit/routers/test_conversations.py index 2e31d19a..e94e90a5 100644 --- a/tests/unit/routers/test_conversations.py +++ b/tests/unit/routers/test_conversations.py @@ -148,18 +148,19 @@ def test_messages_endpoint( assert ( b'{"event": "agent_action", "data": {"agent": "KymaAgent", "answer": {"content": ' - b'"To create an API Rule in Kyma to expose a service externally"}}}\n' + b'"To create an API Rule in Kyma to expose a service externally", "tasks": []}}}\n' in content ) assert ( b'{"event": "agent_action", "data": {"agent": "KubernetesAgent", "answer": {"content": ' - b'"To create a kubernetes deployment"}}}\n' in content + b'"To create a kubernetes deployment", "tasks": []}}}\n' in content ) assert ( b'{"event": "agent_action", "data": {"agent": "KymaAgent", "answer": {"content' - b'": "To create an API Rule in Kyma to expose a service externally"}}}\n' - b'{"event": "agent_action", "data": {"agent": "KubernetesAgent", "answer": {"content": ' - b'"To create a kubernetes deployment"}}}\n' in content + b'": "To create an API Rule in Kyma to expose a service externally", "tasks": ' + b'[]}}}\n{"event": "agent_action", "data": {"agent": "KubernetesAgent", "an' + b'swer": {"content": "To create a kubernetes deployment", "tasks": []}}}\n' + in content ) diff --git a/tests/unit/utils/test_response.py b/tests/unit/utils/test_response.py index 38b840bb..850745b2 100644 --- a/tests/unit/utils/test_response.py +++ b/tests/unit/utils/test_response.py @@ -2,73 +2,90 @@ import pytest -from utils.response import prepare_chunk_response +from utils.response import prepare_chunk_response, reformat_subtasks @pytest.mark.parametrize( "input, expected", [ ( + # Agent Response with tasks # input - b'{"Planner": {"messages": [{"content": "Task decomposed into subtasks and assigned ' - b'to agents: [{\\"description\\": \\"Create a Hello World Python Serverless ' - b'Function in Kyma.\\", \\"assigned_to\\": \\"KymaAgent\\", \\"status\\": ' - b'\\"pending\\", \\"result\\": null}, {\\"description\\": \\"Create an API ' - b'Rule to expose the Python Serverless Function externally.\\", \\"assigned_to\\": ' - b'\\"KymaAgent\\", \\"status\\": \\"pending\\", \\"result\\": null}]", "additional_kwargs": {}, ' - b'"response_metadata": {}, "type": "ai", "name": "Planner", "id": null, "example": false, ' - b'"tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}], "next": "Continue", ' - b'"subtasks": [{"description": "Create a Hello World Python Serverless Function in Kyma.", ' - b'"assigned_to": "KymaAgent", "status": "pending", "result": null}, {"description": ' - b'"Create an API Rule to expose the Python Serverless Function externally.", "assigned_to": ' - b'"KymaAgent", "status": "pending", "result": null}], "final_response": null, "error": null}}', + b'{"KubernetesAgent": { "messages": [{"additional_kwargs": {}, "content": "The Pod named is running and ready.", ' + b'"name": "KubernetesAgent"}], ' + b'"subtasks": ' + b'[{"assigned_to": "KubernetesAgent", "description": "what is my pods status", "status": "completed", "task_title": "Checking status of pods"}, ' + b'{"assigned_to": "Common", "description": "how to write hello world code in python", "status": "pending", "task_title": "Retrieving hello world code in python"}, ' + b'{"assigned_to": "KymaAgent", "description": "how to create kyma function", "status": "pending", "task_title": "Fetching steps to create kyma function"}], "next": "__end__"}}', # expected - b'{"event": "agent_action", "data": {"agent": "Planner", "answer": {"content": ' - b'"Task decomposed into subtasks and assigned to agents: [{\\"description\\": ' - b'\\"Create a Hello World Python Serverless Function in Kyma.\\", \\"assigned_to\\": ' - b'\\"KymaAgent\\", \\"status\\": \\"pending\\", \\"result\\": null}, {\\"description\\": ' - b'\\"Create an API Rule to expose the Python Serverless Function externally.\\", ' - b'\\"assigned_to\\": \\"KymaAgent\\", \\"status\\": \\"pending\\", \\"result\\": null}]", ' - b'"subtasks": [{"description": "Create a Hello World Python Serverless ' - b'Function in Kyma.", "assigned_to": "KymaAgent", "status": "pending", ' - b'"result": null}, {"description": "Create an API Rule to expose the Python ' - b'Serverless Function externally.", "assigned_to": "KymaAgent", "status": "pending", ' - b'"result": null}]}}}', + b'{"event": "agent_action", "data": {"agent": "KubernetesAgent", "answer": {"c' + b'ontent": "The Pod named is running and ready.", "tasks": [{"task_id": 0, "ta' + b'sk_name": "Checking status of pods", "status": "completed", "agent": "Kubern' + b'etesAgent"}, {"task_id": 1, "task_name": "Retrieving hello world code in pyt' + b'hon", "status": "pending", "agent": "Common"}, {"task_id": 2, "task_name": "' + b'Fetching steps to create kyma function", "status": "pending", "agent": "Kyma' + b'Agent"}], "next": "Common"}}}', ), ( + # Supervisor Agent with last agent planner # input - b'{"Supervisor": {"messages": [{"content": "{\\n \\"next\\": \\"KymaAgent\\"\\n}", ' - b'"additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": "Supervisor", ' - b'"id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}], ' - b'"next": "KymaAgent", "subtasks": ' - b'[{"description": "Create a Hello World Python Serverless Function in Kyma.", "assigned_to": ' - b'"KymaAgent", "status": "pending", "result": null}, {"description": ' - b'"Create an API Rule to expose the Python Serverless Function externally.", ' - b'"assigned_to": "KymaAgent", "status": "pending", "result": null}]}}', + b'{"Supervisor": { "messages": [{"additional_kwargs": {}, "content": "", ' + b'"name": "Planner"}], ' + b'"subtasks": ' + b'[{"assigned_to": "KubernetesAgent", "description": "what is my pods status", "status": "pending", "task_title": "Checking status of pods"}, ' + b'{"assigned_to": "Common", "description": "how to write hello world code in python", "status": "pending", "task_title": "Retrieving hello world code in python"}, ' + b'{"assigned_to": "KymaAgent", "description": "how to create kyma function", "status": "pending", "task_title": "Fetching steps to create kyma function"}], "next": "KubernetesAgent"}}', # expected - b'{"event": "agent_action", "data": {"agent": "Supervisor", "answer": ' - b'{"content": "{\\n \\"next\\": \\"KymaAgent\\"\\n}", ' - b'"next": "KymaAgent"}}}', + b'{"event": "agent_action", "data": {"agent": "Supervisor", "answer": {"c' + b'ontent": "", "tasks": [{"task_id": 0, "ta' + b'sk_name": "Checking status of pods", "status": "pending", "agent": "Kubern' + b'etesAgent"}, {"task_id": 1, "task_name": "Retrieving hello world code in pyt' + b'hon", "status": "pending", "agent": "Common"}, {"task_id": 2, "task_name": "' + b'Fetching steps to create kyma function", "status": "pending", "agent": "Kyma' + b'Agent"}], "next": "KubernetesAgent"}}}', ), ( + # Supervisor Agent with last agent finalizer # input - b'{"KymaAgent": {"messages": [{"content": "To create an API Rule in Kyma to ' - b'expose a service externally", "additional_kwargs": {}, "response_metadata": {}, ' - b'"type": "ai", "name": "Supervisor", "id": null, "example": false, "tool_calls": [], ' - b'"invalid_tool_calls": [], "usage_metadata": null}]}}', + b'{"Supervisor": { "messages": [{"additional_kwargs": {}, "content": "final response", ' + b'"name": "Finalizer"}], ' + b'"subtasks": ' + b'[{"assigned_to": "KubernetesAgent", "description": "what is my pods status", "status": "completed", "task_title": "Checking status of pods"}, ' + b'{"assigned_to": "Common", "description": "how to write hello world code in python", "status": "completed", "task_title": "Retrieving hello world code in python"}, ' + b'{"assigned_to": "KymaAgent", "description": "how to create kyma function", "status": "completed", "task_title": "Fetching steps to create kyma function"}], "next": "__end__"}}', # expected - b'{"event": "agent_action", "data": {"agent": "KymaAgent", "answer": {"content": ' - b'"To create an API Rule in Kyma to expose a service externally"}}}', + b'{"event": "agent_action", "data": {"agent": "Supervisor", "answer": {"c' + b'ontent": "final response", "tasks": [{"task_id": 0, "ta' + b'sk_name": "Checking status of pods", "status": "completed", "agent": "Kubern' + b'etesAgent"}, {"task_id": 1, "task_name": "Retrieving hello world code in pyt' + b'hon", "status": "completed", "agent": "Common"}, {"task_id": 2, "task_name": "' + b'Fetching steps to create kyma function", "status": "completed", "agent": "Kyma' + b'Agent"}], "next": "__end__"}}}', ), ( + # Skip response from supervisor if agent is not planner or not finalizer # input - b'{"KubernetesAgent": {"messages": [{"content": "To create a kubernetes deployment", ' - b'"additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": "Supervisor", ' - b'"id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}]}}', + b'{"Supervisor": { "messages": [{"additional_kwargs": {}, "content": "The Pod named is running and ready.", ' + b'"name": "KubernetesAgent"}], ' + b'"subtasks": ' + b'[{"assigned_to": "KubernetesAgent", "description": "what is my pods status", "status": "completed", "task_title": "Checking status of pods"}, ' + b'{"assigned_to": "Common", "description": "how to write hello world code in python", "status": "pending", "task_title": "Retrieving hello world code in python"}, ' + b'{"assigned_to": "KymaAgent", "description": "how to create kyma function", "status": "pending", "task_title": "Fetching steps to create kyma function"}], "next": "KymaAgent"}}', # expected - b'{"event": "agent_action", "data": {"agent": "KubernetesAgent", ' - b'"answer": {"content": "To create a kubernetes deployment"}}}', + None, ), + ( + # Direct response from planner + # input + b'{"Supervisor": {"messages": [{"additional_kwargs": {}, "content": "The capital of India is New Delhi.", ' + b'"name": "Planner", "response_metadata": {}, "tool_calls": []}], ' + b'"subtasks": [], "next": "__end__", "error": null}}', + # expected + b'{"event": "agent_action", "data": {"agent": "Supervisor", "answer": {"conten' + b't": "The capital of India is New Delhi.", "tasks": [], "next": "__end__"}}}', + ), + # Skip response if agent is Summarization + (b'{"Summarization": {"messages": []}}', None), ( # input b'{"KubernetesAgent": {"error": "Error occurred"}}', @@ -76,6 +93,7 @@ b'{"event": "agent_action", "data": {"agent": "KubernetesAgent", "error": "Error occurred"}}', ), ( + # Error response for invalid json # input b'{"InvalidJSON"', # expected @@ -92,3 +110,96 @@ @patch("utils.response.get_logger", Mock()) def test_prepare_chunk_response(input, expected): assert prepare_chunk_response(input) == expected + + +@pytest.mark.parametrize( + "subtasks, expected_output", + [ + # Test case 1: Empty subtasks + ([], []), + # Test case 2: Single subtask + ( + [ + { + "assigned_to": "KubernetesAgent", + "description": "what is my pods status", + "status": "completed", + "task_title": "Checking status of pods", + } + ], + [ + { + "task_id": 0, + "task_name": "Checking status of pods", + "status": "completed", + "agent": "KubernetesAgent", + } + ], + ), + # Test case 3: Multiple subtasks + ( + [ + { + "assigned_to": "KubernetesAgent", + "description": "what is my pods status", + "status": "completed", + "task_title": "Checking status of pods", + }, + { + "assigned_to": "Common", + "description": "how to write hello world code in python", + "status": "pending", + "task_title": "Retrieving hello world code in python", + }, + { + "assigned_to": "KymaAgent", + "description": "how to create kyma function", + "status": "pending", + "task_title": "Fetching steps to create kyma function", + }, + ], + [ + { + "task_id": 0, + "task_name": "Checking status of pods", + "status": "completed", + "agent": "KubernetesAgent", + }, + { + "task_id": 1, + "task_name": "Retrieving hello world code in python", + "status": "pending", + "agent": "Common", + }, + { + "task_id": 2, + "task_name": "Fetching steps to create kyma function", + "status": "pending", + "agent": "KymaAgent", + }, + ], + ), + # Test case 4: Missing keys (should raise KeyError) + ( + [ + { + "assigned_to": "KubernetesAgent", + "description": "what is my pods status", + "status": "completed", + } + ], + KeyError, + ), + # Test case 5: Incorrect data types (should raise TypeError) + ("not a dictionary", TypeError), + # Test case 6: None input + (None, []), + ], +) +def test_reformat_subtasks(subtasks, expected_output): + if expected_output in [KeyError, TypeError]: + with pytest.raises(expected_output): + reformat_subtasks(subtasks) + else: + result = reformat_subtasks(subtasks) + assert result == expected_output