Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ build/
*.egg-info/
.env
.pytest_cache/
.mypy_cache/
.ruff_cache/
.venv/
venv/
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,72 @@ while True:
page = client.messages.list(mailbox_id=mailbox.id, limit=50, cursor=page.next_cursor)
```

## 프레임워크 통합 (LangChain / CrewAI)

LoftBox 도구를 LLM 에이전트 프레임워크에 그대로 꽂을 수 있습니다. 노출 도구:
`send_email`, `check_inbox`, `list_messages`, `approve_message`, `reject_message`.

각 프레임워크는 선택 의존성으로 설치합니다(미설치 시에도 base SDK 는 정상 동작):

```bash
pip install loftbox[langchain] # LangChain
pip install loftbox[crewai] # CrewAI
```

### LangChain

```python
from loftbox import LoftBox
from loftbox.integrations.langchain import LoftBoxToolkit

client = LoftBox(api_key="lb_live_xxx")
tools = LoftBoxToolkit(client).get_tools()

# tools 를 LangChain 에이전트에 전달
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_messages([
("system", "너는 이메일을 다루는 에이전트다."),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
AgentExecutor(agent=agent, tools=tools).invoke(
{"input": "mb_123 받은편지함에 새 메일 있는지 확인해줘"}
)
```

전체 예제: `examples/langchain_agent.py`.

### CrewAI

```python
from loftbox import LoftBox
from loftbox.integrations.crewai import get_crewai_tools
from crewai import Agent, Task, Crew

client = LoftBox(api_key="lb_live_xxx")
tools = get_crewai_tools(client)

support = Agent(
role="이메일 지원 담당",
goal="받은편지함을 확인하고 답장한다",
backstory="LoftBox 메일박스를 운영하는 에이전트.",
tools=tools,
)
task = Task(
description="mb_123 받은편지함을 확인하고 요약하라",
agent=support,
expected_output="새 메일 요약",
)
Crew(agents=[support], tasks=[task]).kickoff()
```

전체 예제: `examples/crewai_agent.py`.

## 예제

`examples/quickstart.py` 참고.
Expand Down
42 changes: 42 additions & 0 deletions examples/crewai_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""LoftBox + CrewAI 최소 예제.

실행 전:
pip install loftbox[crewai]
export LOFTBOX_API_KEY=lb_live_xxx
export OPENAI_API_KEY=sk-xxx # CrewAI 기본 LLM
"""

from __future__ import annotations

import os

from loftbox import LoftBox
from loftbox.integrations.crewai import get_crewai_tools


def main() -> None:
client = LoftBox(api_key=os.environ["LOFTBOX_API_KEY"])
tools = get_crewai_tools(client)

print(f"LoftBox 도구 {len(tools)}개: {[t.name for t in tools]}")

# 실제 에이전트 연결 예 (선택 — crewai + LLM 필요):
#
# from crewai import Agent, Task, Crew
#
# support = Agent(
# role="이메일 지원 담당",
# goal="받은편지함을 확인하고 답장한다",
# backstory="LoftBox 메일박스를 운영하는 에이전트.",
# tools=tools,
# )
# task = Task(
# description="mb_123 받은편지함을 확인하고 요약하라",
# agent=support,
# expected_output="새 메일 요약",
# )
# Crew(agents=[support], tasks=[task]).kickoff()


if __name__ == "__main__":
main()
41 changes: 41 additions & 0 deletions examples/langchain_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""LoftBox + LangChain 최소 예제.

실행 전:
pip install loftbox[langchain] langchain-openai
export LOFTBOX_API_KEY=lb_live_xxx
export OPENAI_API_KEY=sk-xxx
"""

from __future__ import annotations

import os

from loftbox import LoftBox
from loftbox.integrations.langchain import LoftBoxToolkit


def main() -> None:
client = LoftBox(api_key=os.environ["LOFTBOX_API_KEY"])
tools = LoftBoxToolkit(client).get_tools()

print(f"LoftBox 도구 {len(tools)}개: {[t.name for t in tools]}")

# 실제 에이전트 연결 예 (선택 — langchain + LLM 필요):
#
# from langchain.agents import create_tool_calling_agent, AgentExecutor
# from langchain_core.prompts import ChatPromptTemplate
# from langchain_openai import ChatOpenAI
#
# llm = ChatOpenAI(model="gpt-4o-mini")
# prompt = ChatPromptTemplate.from_messages([
# ("system", "너는 이메일을 다루는 에이전트다."),
# ("human", "{input}"),
# ("placeholder", "{agent_scratchpad}"),
# ])
# agent = create_tool_calling_agent(llm, tools, prompt)
# executor = AgentExecutor(agent=agent, tools=tools)
# executor.invoke({"input": "mb_123 받은편지함에 새 메일 있는지 확인해줘"})


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion loftbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
Webhook,
)

__version__ = "0.1.0"
__version__ = "0.2.0"
__all__ = [
"LoftBox",
# models
Expand Down
2 changes: 1 addition & 1 deletion loftbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

DEFAULT_BASE_URL = "https://api.loftbox.net"
DEFAULT_TIMEOUT = 30.0
USER_AGENT = "loftbox-python/0.1.0"
USER_AGENT = "loftbox-python/0.2.0"


class LoftBox:
Expand Down
17 changes: 17 additions & 0 deletions loftbox/integrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""프레임워크 통합 — LangChain / CrewAI 도구.

서브모듈은 lazy/guarded import 한다. 해당 프레임워크가 설치돼 있지 않아도
`import loftbox` 와 base SDK 는 깨지지 않는다. 통합을 쓰려면:

pip install loftbox[langchain] # LangChain
pip install loftbox[crewai] # CrewAI

서브모듈 import 시점에 프레임워크 미설치면 친절한 ImportError 를 던진다:

from loftbox.integrations.langchain import LoftBoxToolkit
from loftbox.integrations.crewai import get_crewai_tools
"""

from __future__ import annotations

__all__ = ["langchain", "crewai"]
168 changes: 168 additions & 0 deletions loftbox/integrations/_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""LangChain / CrewAI 도구가 공유하는 인자 스키마 + 실행 로직.

도구별로 pydantic args_schema 와 실제 SDK 호출을 한곳에 정의해, 두 프레임워크
어댑터가 같은 동작을 갖도록 한다. SDK 가 반환하는 모델은 사람이 읽을 수 있는
문자열로 요약해서 LLM 컨텍스트에 넣는다.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, List, Optional

from pydantic import BaseModel, Field

if TYPE_CHECKING:
from ..client import LoftBox
from ..models import Message, Page


# -- 인자 스키마 ------------------------------------------------------------


class SendEmailArgs(BaseModel):
"""이메일 발송 인자."""

mailbox_id: str = Field(description="발송에 사용할 메일박스 ID (mb_...)")
to: List[str] = Field(description="수신자 이메일 주소 목록")
subject: str = Field(description="이메일 제목")
body_text: Optional[str] = Field(
default=None, description="평문 본문. body_html 과 최소 하나는 필요."
)
body_html: Optional[str] = Field(default=None, description="HTML 본문(선택)")
cc: Optional[List[str]] = Field(default=None, description="참조(cc) 주소 목록(선택)")
in_reply_to: Optional[str] = Field(
default=None, description="답장 시 원본 메시지의 Message-ID(선택)"
)


class CheckInboxArgs(BaseModel):
"""미확인 수신 메시지 폴링 인자."""

mailbox_id: str = Field(description="조회할 메일박스 ID (mb_...)")
limit: Optional[int] = Field(default=None, description="가져올 최대 개수(선택)")
cursor: Optional[str] = Field(default=None, description="다음 페이지 커서(선택)")


class ListMessagesArgs(BaseModel):
"""메시지 목록 조회 인자."""

mailbox_id: Optional[str] = Field(default=None, description="메일박스 ID 필터(선택)")
direction: Optional[str] = Field(
default=None, description="방향 필터: 'incoming' 또는 'outgoing'(선택)"
)
status: Optional[str] = Field(default=None, description="상태 필터(선택)")
q: Optional[str] = Field(default=None, description="전문 검색어(선택)")
limit: Optional[int] = Field(default=None, description="최대 개수(선택)")
cursor: Optional[str] = Field(default=None, description="다음 페이지 커서(선택)")


class ApproveMessageArgs(BaseModel):
"""발송 승인 인자."""

message_id: str = Field(description="승인할 메시지 ID (msg_...)")
reason: str = Field(description="승인 사유 (감사 로그에 기록됨)")


class RejectMessageArgs(BaseModel):
"""발송 거부 인자."""

message_id: str = Field(description="거부할 메시지 ID (msg_...)")
reason: str = Field(description="거부 사유 (감사 로그에 기록됨)")


# -- 도구 메타 + 실행 -------------------------------------------------------

SEND_EMAIL_DESCRIPTION = (
"LoftBox 메일박스에서 이메일을 발송한다. mailbox_id, 수신자(to), 제목(subject)이 "
"필요하며 body_text 또는 body_html 중 하나 이상을 줘야 한다. 답장이면 in_reply_to 에 "
"원본 Message-ID 를 넣는다."
)
CHECK_INBOX_DESCRIPTION = (
"메일박스의 미확인(unacked) 수신 메시지를 폴링한다. 새로 도착한 이메일을 확인할 때 쓴다."
)
LIST_MESSAGES_DESCRIPTION = (
"메시지 목록을 조회한다. mailbox_id/direction/status 로 필터하거나 q 로 전문 검색한다."
)
APPROVE_MESSAGE_DESCRIPTION = "승인 대기 중인 발송 메시지를 승인한다. 사유(reason)가 필요하다."
REJECT_MESSAGE_DESCRIPTION = "승인 대기 중인 발송 메시지를 거부한다. 사유(reason)가 필요하다."


def _summarize_message(msg: "Message") -> str:
parts = [f"id={msg.id}"]
if msg.status:
parts.append(f"status={msg.status}")
if msg.subject:
parts.append(f"subject={msg.subject!r}")
return "Message(" + ", ".join(parts) + ")"


def _summarize_page(page: "Page") -> str:
if not page.data:
return "메시지 없음."
lines = [_summarize_message(m) for m in page.data]
out = f"{len(page.data)}건:\n" + "\n".join(lines)
if page.next_cursor:
out += f"\nnext_cursor={page.next_cursor}"
return out


def run_send_email(
client: "LoftBox",
mailbox_id: str,
to: List[str],
subject: str,
body_text: Optional[str] = None,
body_html: Optional[str] = None,
cc: Optional[List[str]] = None,
in_reply_to: Optional[str] = None,
) -> str:
msg = client.messages.send(
mailbox_id=mailbox_id,
to=to,
subject=subject,
body_text=body_text,
body_html=body_html,
cc=cc,
in_reply_to=in_reply_to,
)
return "발송됨: " + _summarize_message(msg)


def run_check_inbox(
client: "LoftBox",
mailbox_id: str,
limit: Optional[int] = None,
cursor: Optional[str] = None,
) -> str:
page = client.mailboxes.list_inbox(mailbox_id, limit=limit, cursor=cursor)
return _summarize_page(page)


def run_list_messages(
client: "LoftBox",
mailbox_id: Optional[str] = None,
direction: Optional[str] = None,
status: Optional[str] = None,
q: Optional[str] = None,
limit: Optional[int] = None,
cursor: Optional[str] = None,
) -> str:
page = client.messages.list(
mailbox_id=mailbox_id,
direction=direction,
status=status,
q=q,
limit=limit,
cursor=cursor,
)
return _summarize_page(page)


def run_approve_message(client: "LoftBox", message_id: str, reason: str) -> str:
msg = client.messages.approve(message_id, reason)
return "승인됨: " + _summarize_message(msg)


def run_reject_message(client: "LoftBox", message_id: str, reason: str) -> str:
msg = client.messages.reject(message_id, reason)
return "거부됨: " + _summarize_message(msg)
Loading
Loading