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
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ clean: ## Removes all generated code (except _patch.py files)
@printf "=== Cleaning src directory\n"
@rm -rf src/pydo/resources
@rm -rf src/pydo/types
@find src/pydo -type f ! -name "_patch.py" ! -name "custom_*.py" ! -name "exceptions.py" -exec rm -rf {} +
@find src/pydo -type f \
! -name "_patch.py" ! -name "custom_*.py" ! -name "exceptions.py" \
! -path "*/gateway/*" \
! -path "*/action_gateway/*" \
-exec rm -rf {} +

.PHONY: download-spec
download-spec: ## Download Latest DO Spec
Expand Down
94 changes: 94 additions & 0 deletions examples/gateway/approval_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Approve Chat Completions tool calls through an Action Gateway session.

Required env:
DIGITALOCEAN_TOKEN

Optional env:
ACTOR_ID
MODEL
PROMPT
"""

import json
import os

from pydo.action_gateway import ActionGatewayClient


def find_approval_ids(value):
"""Find approval IDs in gateway tool-result messages."""
approval_ids = []
if isinstance(value, dict):
for key in ("approval_id", "approvalId"):
if value.get(key):
approval_ids.append(value[key])
for nested in value.values():
approval_ids.extend(find_approval_ids(nested))
elif isinstance(value, list):
for nested in value:
approval_ids.extend(find_approval_ids(nested))
elif isinstance(value, str):
try:
approval_ids.extend(find_approval_ids(json.loads(value)))
except json.JSONDecodeError:
pass
return approval_ids


client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"], timeout=30)
print("Creating Action Gateway session...")
session = client.session.create(
actor_id=os.environ.get("ACTOR_ID", "example-user"),
permissions={
"default_action": "ask",
"rules": [{"tool": "action_search", "action": "allow"}],
},
)

model = os.environ.get("MODEL", "openai-gpt-4o")
messages = [
{
"role": "user",
"content": os.environ.get(
"PROMPT",
"Search for the latest DigitalOcean news and summarize it.",
),
}
]
tools = session.tools()
tool_choice = "required"

while True:
print(f"Requesting next tool call from {model}...")
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
tool_choice=tool_choice,
parallel_tool_calls=False,
)
message = response.choices[0].message
if not message.get("tool_calls"):
break

print("Executing requested gateway tool...")
tool_messages = session.handle_tool_calls(response)
approval_ids = list(dict.fromkeys(find_approval_ids(tool_messages)))
for approval_id in approval_ids:
input(f"Approve {approval_id}? Press Enter to continue...")
session.approve(approval_id)

if approval_ids:
print("Retrying approved gateway tool...")
tool_messages = session.handle_tool_calls(response)

messages.append(dict(message))
messages.extend(tool_messages)
if any(
tool_call["function"]["name"] != "action_search"
for tool_call in message["tool_calls"]
):
tool_choice = "auto"

print("\nFinal answer:\n")
print(message.get("content"))
45 changes: 45 additions & 0 deletions examples/gateway/async_invoke_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Async Action Gateway session: list, invoke, and execute code.

Required env:
DIGITALOCEAN_TOKEN

Optional env:
PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run
ACTOR_ID
"""

import asyncio
import os

from pydo.action_gateway.aio import ActionGatewayClient


async def main() -> None:
client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"])
session = await client.session.create(
actor_id=os.environ.get("ACTOR_ID", "example-user"),
permissions={
"default_action": "ask",
"rules": [
{"tool": "exa_web_search", "action": "allow"},
{"tool": "execute_code", "action": "allow"},
],
},
)

tools = await session.tools.list(include_all=True)
print("session tools:", [tool.name for tool in tools])
print("MCP URL:", session.url)

output = await session.tools.invoke_one(
"exa_web_search", {"query": "DigitalOcean Gradient", "max_results": 2}
)
print("web_search output:", str(output)[:200])

result = await session.code.execute("print('hello from async')")
print("code stdout:", result.get("stdout"))

await client.close()


asyncio.run(main())
25 changes: 25 additions & 0 deletions examples/gateway/create_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Create an Action Gateway session and print its MCP URL.

Required env:
DIGITALOCEAN_TOKEN

Optional env:
ACTOR_ID
"""

import os

from pydo.action_gateway import ActionGatewayClient

client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"])
session = client.session.create(
actor_id=os.environ.get("ACTOR_ID", "example-user"),
tools=["exa_web_search@v1"],
config={"preloadTools": ["exa_web_search@v1"]},
permissions={
"default_action": "ask",
"rules": [{"tool": "exa_web_search", "action": "allow"}],
},
)

print(session.url)
36 changes: 36 additions & 0 deletions examples/gateway/create_toolbelt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Create a versioned Action Gateway toolbelt.

Required env:
DIGITALOCEAN_TOKEN
"""

import os

from pydo.action_gateway import ActionGatewayClient

client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"])

toolbelt = client.create_toolbelt(
name="search-toolbelt",
tools=[
"exa_web_search",
"exa_web_fetch",
],
)

print(toolbelt.ref)

# Public Tool Registry APIs are generated from DigitalOcean's OpenAPI spec.
print(client.toolbelts.list(status="active"))
print(client.toolbelts.get("search-toolbelt", version="1"))
client.toolbelts.add_tools(
"search-toolbelt",
body={"tools": ["jira_create_issue"]},
)
client.toolbelts.delete_tools(
"search-toolbelt",
body={"tools": ["exa_web_fetch"]},
)

# Delete the toolbelt when it is no longer needed.
# client.toolbelts.delete("search-toolbelt")
34 changes: 34 additions & 0 deletions examples/gateway/execute_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Run Python code in the Action Gateway sandbox (action_code).

Required env:
DIGITALOCEAN_TOKEN

Optional env:
PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run
ACTOR_ID
"""

import os

from pydo.action_gateway import ActionGatewayClient

client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"])
session = client.session.create(
actor_id=os.environ.get("ACTOR_ID", "example-user"),
permissions={
"default_action": "ask",
"rules": [{"tool": "execute_code", "action": "allow"}],
},
)

result = session.code.execute(
"import sys\n" "print('hello from the sandbox')\n" "print(sys.version)\n",
thought="verify the sandbox works",
)

print("exit_code:", result.get("exit_code"))
print("stdout:")
print(result.get("stdout"))
if result.get("stderr"):
print("stderr:")
print(result.get("stderr"))
57 changes: 57 additions & 0 deletions examples/gateway/function_calling_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Agentic function-calling loop: chat completions + Action Gateway session.

Required env:
DIGITALOCEAN_TOKEN

Optional env:
PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run
ACTOR_ID
MODEL
PROMPT
"""

import os

from pydo.action_gateway import ActionGatewayClient

client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"])
session = client.session.create(
actor_id=os.environ.get("ACTOR_ID", "example-user"),
permissions={
"default_action": "ask",
"rules": [
{"tool": "exa_web_search", "action": "allow"},
{"tool": "exa_web_fetch", "action": "allow"},
],
},
)

model = os.environ.get("MODEL", "openai-gpt-5.4")
prompt = os.environ.get("PROMPT", "Search the web for information on DigitalOcean.")

tools = session.tools()
messages = [{"role": "user", "content": prompt}]
tool_choice = "required"

while True:
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
tool_choice=tool_choice,
)
message = response.choices[0].message
if not message.get("tool_calls"):
break

messages.append(dict(message))
tool_messages = session.handle_tool_calls(response)
if any(
tool_call["function"]["name"] != "action_search"
for tool_call in message["tool_calls"]
):
tool_choice = "auto"
messages.extend(tool_messages)

print("\nFinal answer:\n")
print(message.get("content"))
54 changes: 54 additions & 0 deletions examples/gateway/invoke_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Invoke Action Gateway tools in parallel (action_invoke).

Required env:
DIGITALOCEAN_TOKEN

Optional env:
PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run
ACTOR_ID
"""

import os

from pydo.action_gateway import ActionGatewayClient

client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"])
session = client.session.create(
actor_id=os.environ.get("ACTOR_ID", "example-user"),
permissions={
"default_action": "ask",
"rules": [
{"tool": "exa_web_search", "action": "allow"},
{"tool": "exa_web_fetch", "action": "allow"},
],
},
)

envelope = session.tools.invoke(
[
{
"tool": "exa_web_search",
"arguments": {"query": "DigitalOcean Gradient", "max_results": 3},
},
{
"tool": "exa_web_fetch",
"arguments": {"url": "https://www.digitalocean.com"},
},
],
rationale="demonstrate parallel tool invocation",
)

print(f"{envelope.success_count}/{envelope.total_count} succeeded\n")
for item in envelope.results:
result = item.result
print(f"[{item.index}] {item.tool}: {result.status}")
if result.status == "succeeded":
print(f" output: {str(result.get('output'))[:200]}")
else:
error = result.get("error", {})
print(f" error ({error.get('class')}): {error.get('message')}")

output = session.tools.invoke_one(
"exa_web_search", {"query": "MCP protocol", "max_results": 1}
)
print("\ninvoke_one output:", str(output)[:200])
31 changes: 31 additions & 0 deletions examples/gateway/list_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""List Action Gateway tools for a session.

By default the session exposes three meta-tools (action_search,
action_invoke, action_code). Pass include_all=True to include tools configured
through config.preloadTools.

Required env:
DIGITALOCEAN_TOKEN

Optional env:
PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run
ACTOR_ID
"""

import os

from pydo.action_gateway import ActionGatewayClient

client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"])
session = client.session.create(
actor_id=os.environ.get("ACTOR_ID", "example-user"),
)

print("MCP URL:", session.url)
print("\nMeta-tools (default):")
for tool in session.tools.list():
print(f" {tool.name}: {tool.get('description', '')[:80]}")

print("\nAll tools exposed by this session MCP endpoint:")
for tool in session.tools.list(include_all=True):
print(f" {tool.name}: {tool.get('description', '')[:80]}")
Loading
Loading