feat: implement gitea_comment and discord_reply tools

This commit is contained in:
머니페니 2026-03-20 17:39:45 +09:00
parent b2ad726fc4
commit e8983d8534
5 changed files with 205 additions and 11 deletions

View File

@ -1,5 +1,32 @@
"""Discord message tool. Phase 2 implementation.""" """Discord 채널/스레드 메시지 전송 도구."""
from __future__ import annotations
import asyncio
import logging
import os
from typing import Any
from agent.utils.discord_client import get_discord_client
logger = logging.getLogger(__name__)
def discord_reply(message: str) -> dict: def discord_reply(message: str) -> dict[str, Any]:
raise NotImplementedError("Phase 2") if not message.strip():
return {"success": False, "error": "빈 메시지는 전송할 수 없습니다."}
channel_id = os.environ.get("DISCORD_CHANNEL_ID", "")
if not channel_id:
return {"success": False, "error": "DISCORD_CHANNEL_ID가 설정되지 않았습니다."}
client = get_discord_client()
try:
result = asyncio.run(
client.send_message(channel_id=channel_id, content=message)
)
logger.info("Sent Discord message to channel %s", channel_id)
return {"success": True, "message_id": result.get("id")}
except Exception as e:
logger.exception("Failed to send Discord message")
return {"success": False, "error": str(e)}

View File

@ -1,5 +1,40 @@
"""Gitea issue/PR comment tool. Phase 2 implementation.""" """Gitea 이슈/PR 코멘트 작성 도구."""
from __future__ import annotations
import asyncio
import logging
import os
from typing import Any
from agent.utils.gitea_client import get_gitea_client
logger = logging.getLogger(__name__)
def gitea_comment(message: str, issue_number: int) -> dict: def _get_repo_info() -> tuple[str, str]:
raise NotImplementedError("Phase 2") owner = os.environ.get("DEFAULT_REPO_OWNER", "quant")
repo = os.environ.get("DEFAULT_REPO_NAME", "galaxis-po")
return owner, repo
def gitea_comment(message: str, issue_number: int) -> dict[str, Any]:
if not issue_number or issue_number <= 0:
return {"success": False, "error": "유효한 issue_number가 필요합니다."}
if not message.strip():
return {"success": False, "error": "빈 메시지는 작성할 수 없습니다."}
owner, repo = _get_repo_info()
client = get_gitea_client()
try:
result = asyncio.run(
client.create_issue_comment(
owner=owner, repo=repo, issue_number=issue_number, body=message
)
)
logger.info("Posted comment on %s/%s#%d", owner, repo, issue_number)
return {"success": True, "comment_id": result.get("id")}
except Exception as e:
logger.exception("Failed to post comment on %s/%s#%d", owner, repo, issue_number)
return {"success": False, "error": str(e)}

View File

@ -1,9 +1,45 @@
"""Discord bot integration. Phase 2 implementation.""" """Discord REST API 클라이언트."""
from __future__ import annotations
import logging
import os
import httpx
logger = logging.getLogger(__name__)
DISCORD_API_BASE = "https://discord.com/api/v10"
class DiscordClient: class DiscordClient:
async def send_message(self, channel_id: str, content: str) -> dict: def __init__(self, token: str):
raise NotImplementedError("Phase 2") self.token = token
self._client = httpx.AsyncClient(
base_url=DISCORD_API_BASE,
headers={"Authorization": f"Bot {self.token}"},
timeout=15.0,
)
async def send_thread_reply(self, channel_id, thread_id, content) -> dict: async def send_message(self, channel_id: str, content: str) -> dict:
raise NotImplementedError("Phase 2") resp = await self._client.post(
f"/channels/{channel_id}/messages",
json={"content": content},
)
resp.raise_for_status()
return resp.json()
async def send_thread_reply(self, channel_id: str, thread_id: str, content: str) -> dict:
return await self.send_message(channel_id=thread_id, content=content)
async def close(self):
await self._client.aclose()
_client: DiscordClient | None = None
def get_discord_client() -> DiscordClient:
global _client
if _client is None:
_client = DiscordClient(token=os.environ.get("DISCORD_TOKEN", ""))
return _client

View File

@ -0,0 +1,48 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
def test_discord_reply_success():
mock_client = MagicMock()
mock_client.send_message = AsyncMock(
return_value={"id": "123456", "content": "test message"}
)
with patch(
"agent.tools.discord_reply.get_discord_client", return_value=mock_client
), patch.dict("os.environ", {"DISCORD_CHANNEL_ID": "999"}):
from agent.tools.discord_reply import discord_reply
result = discord_reply(message="test message")
assert result["success"] is True
mock_client.send_message.assert_called_once()
def test_discord_reply_empty_message():
from agent.tools.discord_reply import discord_reply
result = discord_reply(message="")
assert result["success"] is False
def test_discord_reply_no_channel_configured():
with patch.dict("os.environ", {"DISCORD_CHANNEL_ID": ""}, clear=False):
from agent.tools.discord_reply import discord_reply
result = discord_reply(message="test")
assert result["success"] is False
assert "DISCORD" in result.get("error", "")
@pytest.mark.asyncio
async def test_discord_client_send_message():
import httpx
mock_resp = MagicMock(spec=httpx.Response)
mock_resp.status_code = 200
mock_resp.json.return_value = {"id": "msg123", "content": "hello"}
mock_resp.raise_for_status = MagicMock()
from agent.utils.discord_client import DiscordClient
client = DiscordClient(token="test-token")
client._client.post = AsyncMock(return_value=mock_resp)
result = await client.send_message(channel_id="999", content="hello")
assert result["id"] == "msg123"
client._client.post.assert_called_once()

View File

@ -0,0 +1,48 @@
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
def test_gitea_comment_success():
mock_client = MagicMock()
mock_client.create_issue_comment = AsyncMock(
return_value={"id": 42, "body": "test comment"}
)
with patch(
"agent.tools.gitea_comment.get_gitea_client", return_value=mock_client
), patch(
"agent.tools.gitea_comment._get_repo_info",
return_value=("quant", "galaxis-po"),
):
from agent.tools.gitea_comment import gitea_comment
result = gitea_comment(message="test comment", issue_number=1)
assert result["success"] is True
assert result["comment_id"] == 42
def test_gitea_comment_missing_issue_number():
from agent.tools.gitea_comment import gitea_comment
result = gitea_comment(message="test", issue_number=0)
assert result["success"] is False
assert "issue_number" in result["error"]
def test_gitea_comment_api_error():
import httpx
mock_client = MagicMock()
mock_client.create_issue_comment = AsyncMock(
side_effect=httpx.HTTPStatusError(
"404", request=MagicMock(), response=MagicMock(status_code=404)
)
)
with patch(
"agent.tools.gitea_comment.get_gitea_client", return_value=mock_client
), patch(
"agent.tools.gitea_comment._get_repo_info",
return_value=("quant", "galaxis-po"),
):
from agent.tools.gitea_comment import gitea_comment
result = gitea_comment(message="test", issue_number=999)
assert result["success"] is False
assert "error" in result