Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2634,3 +2634,14 @@
- `POSTGRES_PASSWORD=change-me-local-only docker compose up -d --build`
- `python scripts/check_compose_logs.py --compose-log-file <captured-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`)를 제거하여 린트 오류를 수정했습니다.
59 changes: 50 additions & 9 deletions backend/api/tools.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import inspect
import json
import logging
from collections.abc import Callable
from typing import Any, Dict, List, Optional
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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])
Expand Down
66 changes: 64 additions & 2 deletions backend/tests/test_tools_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Loading