From 1a88b0355a121a70b02873b81b9b8132724fa0d8 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 10 Oct 2025 14:17:42 +0300 Subject: [PATCH] deep q/a skill and skill selection --- src/haiku/rag/a2a.py | 45 ++++++++++++++++++++-- tests/test_a2a.py | 91 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 3 deletions(-) diff --git a/src/haiku/rag/a2a.py b/src/haiku/rag/a2a.py index 18322da1..f67cffce 100644 --- a/src/haiku/rag/a2a.py +++ b/src/haiku/rag/a2a.py @@ -228,10 +228,44 @@ def get_agent_skills() -> list[Skill]: "Find information about Python best practices", "Show me the full API documentation", ], - ) + ), + Skill( + id="deep-qa", + name="Deep Question Answering", + description="Multi-step question decomposition and research for complex queries (can take a long time)", + tags=["question-answering", "research", "multi-agent", "complex-queries"], + input_modes=["application/json"], + output_modes=["application/json"], + examples=[ + "What are the architectural patterns used in haiku.rag and how do they compare?", + "Analyze the trade-offs between the simple QA and research agents", + "What are all the configuration options and their effects?", + ], + ), ] +def extract_skill_preference(task_history: list[Message]) -> str: + """Extract skill preference from task history metadata. + + Args: + task_history: Task history messages + + Returns: + Skill ID if found in metadata, otherwise "document-qa" (default) + """ + for msg in task_history: + if msg.get("role") == "user": + for part in msg.get("parts", []): + if part.get("kind") == "data": + metadata = part.get("metadata", {}) + if metadata.get("type") == "skill_preference": + skill = part.get("data", {}).get("skill") + if skill: + return skill + return "document-qa" + + def extract_question_from_task(task_history: list[Message]) -> str | None: """Extract the user's question from task history. @@ -340,12 +374,17 @@ def create_a2a_app(db_path: Path): await self.storage.update_task(task["id"], state="working") - # Extract the user's question - question = extract_question_from_task(task.get("history", [])) + # Extract skill preference and question + task_history = task.get("history", []) + skill = extract_skill_preference(task_history) + question = extract_question_from_task(task_history) + if not question: await self.storage.update_task(task["id"], state="failed") return + logger.info(f"Task {task['id']} using skill: {skill}") + try: # Load conversation context context = await self.storage.load_context(task["context_id"]) or [] diff --git a/tests/test_a2a.py b/tests/test_a2a.py index 9dc0cac8..6674e947 100644 --- a/tests/test_a2a.py +++ b/tests/test_a2a.py @@ -4,6 +4,8 @@ import pytest from haiku.rag.a2a import ( extract_question_from_task, + extract_skill_preference, + get_agent_skills, load_message_history, save_message_history, ) @@ -260,3 +262,92 @@ async def test_a2a_app_has_skills(temp_db_path): keyword in skill_text for keyword in ["search", "question", "answer", "document", "knowledge"] ) + + +def test_get_agent_skills(): + """Test that agent skills include both document-qa and deep-qa.""" + skills = get_agent_skills() + + assert len(skills) == 2 + + skill_ids = [skill["id"] for skill in skills] + assert "document-qa" in skill_ids + assert "deep-qa" in skill_ids + + # Check document-qa skill + doc_qa = next(s for s in skills if s["id"] == "document-qa") + assert "Document Question Answering" in doc_qa["name"] + assert "semantic search" in doc_qa["description"] + assert "question-answering" in doc_qa["tags"] + + # Check deep-qa skill + deep_qa = next(s for s in skills if s["id"] == "deep-qa") + assert "Deep Question Answering" in deep_qa["name"] + assert "Multi-step" in deep_qa["description"] + assert "research" in deep_qa["tags"] + + +@pytest.mark.asyncio +async def test_extract_skill_preference_with_metadata(): + """Test extracting skill preference from message metadata.""" + from fasta2a.schema import DataPart + + task_history: list[Message] = [ + Message( + role="user", + parts=[ + TextPart(kind="text", text="Complex question"), + DataPart( + kind="data", + data={"skill": "deep-qa"}, + metadata={"type": "skill_preference"}, + ), + ], + kind="message", + message_id=str(uuid.uuid4()), + ) + ] + + skill = extract_skill_preference(task_history) + assert skill == "deep-qa" + + +@pytest.mark.asyncio +async def test_extract_skill_preference_default(): + """Test that skill preference defaults to document-qa.""" + task_history: list[Message] = [ + Message( + role="user", + parts=[TextPart(kind="text", text="What is Python?")], + kind="message", + message_id=str(uuid.uuid4()), + ) + ] + + skill = extract_skill_preference(task_history) + assert skill == "document-qa" + + +@pytest.mark.asyncio +async def test_extract_skill_preference_no_skill_in_data(): + """Test skill preference when DataPart exists but has no skill.""" + from fasta2a.schema import DataPart + + task_history: list[Message] = [ + Message( + role="user", + parts=[ + TextPart(kind="text", text="Question"), + DataPart( + kind="data", + data={"other": "value"}, + metadata={"type": "skill_preference"}, + ), + ], + kind="message", + message_id=str(uuid.uuid4()), + ) + ] + + skill = extract_skill_preference(task_history) + assert skill == "document-qa"