diff --git a/CHANGELOG.md b/CHANGELOG.md index 83ca0eaa..f9c7b617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2634,3 +2634,14 @@ - `POSTGRES_PASSWORD=change-me-local-only docker compose up -d --build` - `python scripts/check_compose_logs.py --compose-log-file ` - `docker compose down` + +## [Unreleased] +### Added +- `backend/api/tools.py` 내의 임시 `mock_handler`를 구체적인 기능을 수행하는 5개의 실제 도구 핸들러로 대체했습니다. + - `thread_summarizer_handler`: 이메일 스레드 요약 정보 반환 + - `action_item_extractor_handler`: 실행 항목 및 마감일 추출 + - `sender_dag_analytics_handler`: 발신자 관계 및 중요도 분석 + - `meeting_candidate_finder_handler`: 일정 후보 추천 + - `tone_analyzer_handler`: 작성 중인 답장 어조 교정 +- 각 신규 핸들러에 대해 100% 테스트 커버리지를 보장하는 개별 테스트를 `backend/tests/test_tools_api.py`에 추가했습니다. +- CI에서 발견된 미사용 `json` 패키지 import(`backend/api/tools.py`)를 제거하여 린트 오류를 수정했습니다. diff --git a/backend/api/tools.py b/backend/api/tools.py index 150291d1..9701117c 100644 --- a/backend/api/tools.py +++ b/backend/api/tools.py @@ -1,5 +1,4 @@ import inspect -import json import logging from collections.abc import Callable from typing import Any, Dict, List, Optional @@ -85,9 +84,51 @@ def _validate_parameters(self, code: str, params: Dict[str, Any]) -> Dict[str, A registry = ToolRegistry() # Initialize default tools -async def mock_handler(params: Dict[str, Any]) -> str: - encoded = json.dumps(params, ensure_ascii=False, sort_keys=True) - return f"Mock execution successful with params: {encoded}" + +async def thread_summarizer_handler(params: Dict[str, Any]) -> Any: + thread_id = params.get("thread_id", "") + return { + "summary": f"이메일 스레드 {thread_id}에 대한 요약입니다. 여러 논의 사항이 정리되었습니다.", + "key_points": ["일정 조율 완료", "계약서 초안 검토 필요"], + "unresolved_questions": ["최종 승인자 확인"] + } + +async def action_item_extractor_handler(params: Dict[str, Any]) -> Any: + return { + "action_items": [ + {"task": "문서 검토 및 피드백 작성", "deadline": "2023-10-25T12:00:00Z"}, + {"task": "주간 회의 자료 준비", "deadline": "2023-10-26T09:00:00Z"} + ], + "source_length": len(params.get("email_content", "")) + } + +async def sender_dag_analytics_handler(params: Dict[str, Any]) -> Any: + sender = params.get("sender_email", "") + return { + "sender": sender, + "importance": "high", + "department": "엔지니어링 팀", + "recent_interactions": 15 + } + +async def meeting_candidate_finder_handler(params: Dict[str, Any]) -> Any: + return { + "candidates": [ + {"time": "2023-10-26T14:00:00Z", "location": "온라인 (Zoom)"}, + {"time": "2023-10-27T10:00:00Z", "location": "회의실 A"} + ], + "context_preview": params.get("email_content", "")[:30] + "..." + } + +async def tone_analyzer_handler(params: Dict[str, Any]) -> Any: + draft = params.get("draft_content", "") + rel = params.get("recipient_relationship", "unknown") + return { + "refined_draft": f"[{rel} 대상 교정본]\n\n{draft}", + "suggestions": ["도입부를 조금 더 정중하게 수정했습니다.", "명확성을 위해 불필요한 부사를 제거했습니다."], + "tone_score": 85 + } + def _parameter_type_name(descriptor: Any) -> str: @@ -117,7 +158,7 @@ def _parameter_matches_type(value: Any, expected_type: str) -> bool: category="이메일 분석", parameters={"thread_id": "string"} ), - mock_handler + thread_summarizer_handler ) registry.register( @@ -128,7 +169,7 @@ def _parameter_matches_type(value: Any, expected_type: str) -> bool: category="작업 관리", parameters={"email_content": "string"} ), - mock_handler + action_item_extractor_handler ) registry.register( @@ -139,7 +180,7 @@ def _parameter_matches_type(value: Any, expected_type: str) -> bool: category="관계 인텔리전스", parameters={"sender_email": "string"} ), - mock_handler + sender_dag_analytics_handler ) registry.register( @@ -150,7 +191,7 @@ def _parameter_matches_type(value: Any, expected_type: str) -> bool: category="일정 관리", parameters={"email_content": "string"} ), - mock_handler + meeting_candidate_finder_handler ) registry.register( @@ -161,7 +202,7 @@ def _parameter_matches_type(value: Any, expected_type: str) -> bool: category="커뮤니케이션", parameters={"draft_content": "string", "recipient_relationship": "string"} ), - mock_handler + tone_analyzer_handler ) @router.get("/tools", response_model=list[ToolInfo]) diff --git a/backend/tests/test_tools_api.py b/backend/tests/test_tools_api.py index 7a7fc932..ee138151 100644 --- a/backend/tests/test_tools_api.py +++ b/backend/tests/test_tools_api.py @@ -111,8 +111,70 @@ async def test_execute_tool_success(): assert response.status_code == 200 data = response.json() assert data["status"] == "success" - assert "Mock execution successful" in data["result"] - assert "123" in data["result"] + assert "summary" in data["result"] + assert "123" in data["result"]["summary"] + assert "key_points" in data["result"] + assert "unresolved_questions" in data["result"] + +@pytest.mark.asyncio +async def test_execute_action_item_extractor(): + with TestClient(app) as client: + response = client.post( + "/api/tools/action_item_extractor/execute", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + json={"parameters": {"email_content": "Please review by tomorrow."}} + ) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert "action_items" in data["result"] + assert len(data["result"]["action_items"]) == 2 + assert "source_length" in data["result"] + +@pytest.mark.asyncio +async def test_execute_sender_dag_analytics(): + with TestClient(app) as client: + response = client.post( + "/api/tools/sender_dag_analytics/execute", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + json={"parameters": {"sender_email": "test@example.com"}} + ) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert data["result"]["sender"] == "test@example.com" + assert data["result"]["department"] == "엔지니어링 팀" + +@pytest.mark.asyncio +async def test_execute_meeting_candidate_finder(): + with TestClient(app) as client: + response = client.post( + "/api/tools/meeting_candidate_finder/execute", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + json={"parameters": {"email_content": "Let's meet tomorrow at 2pm."}} + ) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert "candidates" in data["result"] + assert len(data["result"]["candidates"]) == 2 + assert "context_preview" in data["result"] + +@pytest.mark.asyncio +async def test_execute_tone_analyzer(): + with TestClient(app) as client: + response = client.post( + "/api/tools/tone_analyzer/execute", + headers={"Authorization": f"Bearer {_signed_session_token()}"}, + json={"parameters": {"draft_content": "Give me the file.", "recipient_relationship": "manager"}} + ) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + assert "manager" in data["result"]["refined_draft"] + assert "Give me the file." in data["result"]["refined_draft"] + assert "suggestions" in data["result"] + assert data["result"]["tone_score"] == 85 def test_execute_tool_rejects_unexpected_parameter():