201 lines
6.3 KiB
Python
201 lines
6.3 KiB
Python
"""
|
|
Custom Tools — local tools + proxy execute with OpenAI Agents SDK.
|
|
|
|
Shows how to create custom tools that run in-process alongside
|
|
remote Composio tools. Includes all three patterns: standalone,
|
|
extension (Gmail proxy), and custom toolkit.
|
|
|
|
Usage:
|
|
COMPOSIO_API_KEY=... OPENAI_API_KEY=... python examples/custom_tools_agent_test.py
|
|
"""
|
|
# ruff: noqa: E302, E303
|
|
|
|
import asyncio
|
|
import base64
|
|
import os
|
|
import sys
|
|
|
|
from agents import Agent, Runner
|
|
from pydantic import BaseModel, Field
|
|
from composio import Composio
|
|
from composio_openai_agents import OpenAIAgentsProvider
|
|
|
|
composio = Composio(
|
|
base_url=os.environ.get("COMPOSIO_BASE_URL"),
|
|
provider=OpenAIAgentsProvider(),
|
|
)
|
|
|
|
# ── 1. Standalone tool — slug/name/description inferred from function ──
|
|
|
|
|
|
class UserLookupInput(BaseModel):
|
|
user_id: str = Field(description="User ID (e.g. user-1)")
|
|
|
|
|
|
USERS = {
|
|
"user-1": {"name": "Alice Johnson", "email": "alice@acme.com", "role": "admin"},
|
|
"user-2": {"name": "Bob Smith", "email": "bob@acme.com", "role": "developer"},
|
|
}
|
|
|
|
|
|
@composio.experimental.tool()
|
|
def get_user(input: UserLookupInput, ctx):
|
|
"""Look up an internal user by ID. Returns name, email, and role."""
|
|
user = USERS.get(input.user_id)
|
|
if not user:
|
|
raise ValueError(f'User "{input.user_id}" not found')
|
|
return user
|
|
|
|
|
|
# ── 2. Standalone tool with overrides ──────────────────────────────────
|
|
|
|
|
|
class GreetInput(BaseModel):
|
|
name: str = Field(description="Name to greet")
|
|
style: str = Field(default="friendly", description="Greeting style")
|
|
|
|
|
|
@composio.experimental.tool(
|
|
slug="GREET_V2",
|
|
name="Greeting Generator",
|
|
description="Generate a personalized greeting message in different styles.",
|
|
)
|
|
def greet(input: GreetInput, ctx):
|
|
"""This docstring is ignored because description= is set above."""
|
|
greetings = {
|
|
"friendly": f"Hey {input.name}! Great to see you!",
|
|
"formal": f"Dear {input.name}, I hope this message finds you well.",
|
|
"casual": f"Yo {input.name}, what's up?",
|
|
}
|
|
return {"message": greetings.get(input.style, f"Hello {input.name}!")}
|
|
|
|
|
|
# ── 3. Extension tool — inherits Gmail auth via ctx.proxy_execute() ────
|
|
|
|
|
|
class DraftInput(BaseModel):
|
|
to: str = Field(description="Recipient email address")
|
|
subject: str = Field(description="Email subject")
|
|
body: str = Field(description="Email body (plain text)")
|
|
|
|
|
|
@composio.experimental.tool(extends_toolkit="gmail")
|
|
def create_draft(input: DraftInput, ctx):
|
|
"""Create a real Gmail draft. Appears in the user's drafts folder."""
|
|
raw_msg = (
|
|
f"To: {input.to}\r\nSubject: {input.subject}\r\n"
|
|
f"Content-Type: text/plain; charset=UTF-8\r\n\r\n{input.body}"
|
|
)
|
|
raw = base64.urlsafe_b64encode(raw_msg.encode()).decode().rstrip("=")
|
|
res = ctx.proxy_execute(
|
|
toolkit="gmail",
|
|
endpoint="https://gmail.googleapis.com/gmail/v1/users/me/drafts",
|
|
method="POST",
|
|
body={"message": {"raw": raw}},
|
|
)
|
|
if res["status"] != 200:
|
|
raise RuntimeError(f"Gmail API error {res['status']}")
|
|
data = res["data"]
|
|
return {"draft_id": data["id"], "to": input.to, "subject": input.subject}
|
|
|
|
|
|
# ── 4. Custom toolkit — groups related tools under one namespace ───────
|
|
|
|
role_manager = composio.experimental.Toolkit(
|
|
slug="ROLE_MANAGER",
|
|
name="Role Manager",
|
|
description="Manage user roles in the system",
|
|
)
|
|
|
|
|
|
class SetRoleInput(BaseModel):
|
|
user_id: str = Field(description="User ID")
|
|
role: str = Field(description="New role (admin, developer, viewer)")
|
|
|
|
|
|
@role_manager.tool()
|
|
def set_role(input: SetRoleInput, ctx):
|
|
"""Set a user's role. Returns confirmation."""
|
|
return {"user_id": input.user_id, "role": input.role, "updated": True}
|
|
|
|
|
|
@role_manager.tool(name="List All Roles")
|
|
def list_roles(input: UserLookupInput, ctx):
|
|
"""List available roles for a user."""
|
|
return {"roles": ["admin", "developer", "viewer"], "current": "admin"}
|
|
|
|
|
|
# ── Agent ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
async def run_test(prompt, test_name):
|
|
print(f"\n{'=' * 60}")
|
|
print(f"TEST: {test_name}")
|
|
print(f"{'=' * 60}")
|
|
|
|
session = composio.create(
|
|
user_id="default",
|
|
toolkits=["gmail", "weathermap"],
|
|
manage_connections=True,
|
|
experimental={
|
|
"custom_tools": [get_user, greet, create_draft],
|
|
"custom_toolkits": [role_manager],
|
|
},
|
|
)
|
|
tools = session.tools()
|
|
agent = Agent(
|
|
name="Assistant",
|
|
instructions=(
|
|
"You are a helpful assistant. Use Composio tools to execute tasks. "
|
|
"In MULTI_EXECUTE, always pass arguments inside the arguments field."
|
|
),
|
|
model="gpt-4.1-mini",
|
|
tools=tools,
|
|
)
|
|
|
|
print(f"> {prompt}\n")
|
|
try:
|
|
result = await Runner.run(agent, prompt, max_turns=25)
|
|
print(f"\nAgent: {result.final_output}")
|
|
return True
|
|
except Exception as e:
|
|
print(f"\nERROR: {e}")
|
|
return False
|
|
|
|
|
|
async def main():
|
|
tests = [
|
|
("Look up user-2", "Standalone (inferred slug/name)"),
|
|
("Generate a formal greeting for Alice", "Standalone (overridden slug/name)"),
|
|
("Set user-1 role to developer", "Toolkit tool"),
|
|
("Look up user-1 and set their role to viewer", "Mixed local tools"),
|
|
("What is the weather in Tokyo?", "Remote (weathermap)"),
|
|
(
|
|
'Draft an email to bob@acme.com with subject "Test" and body "Hello!"',
|
|
"Gmail proxy",
|
|
),
|
|
(
|
|
"Look up user-1, draft them an email saying hi, include weather in SF",
|
|
"Mixed all",
|
|
),
|
|
]
|
|
|
|
results = []
|
|
for prompt, name in tests:
|
|
ok = await run_test(prompt, name)
|
|
results.append((name, ok))
|
|
|
|
print(f"\n{'=' * 60}")
|
|
print("RESULTS")
|
|
print(f"{'=' * 60}")
|
|
for name, ok in results:
|
|
print(f" [{'PASS' if ok else 'FAIL'}] {name}")
|
|
print(f"\n{sum(1 for _, ok in results if ok)}/{len(results)} passed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if not os.environ.get("OPENAI_API_KEY"):
|
|
print("Error: OPENAI_API_KEY env var required")
|
|
sys.exit(1)
|
|
asyncio.run(main())
|