Loading the notebook…
Loading the notebook…
Notebooks · Workspace, live UI and observability · 69
Google's Gemma 4 does native function calling on Bedrock, and shipit routes it transparently through the one class you already know — BedrockChatLLM. This notebook goes end to end:
Its imports and arguments are checked against shipit-agent 3.0. The outputs were saved when it was written, for an earlier release.
from pathlib import Path
import sys
ROOT = (
Path.cwd().resolve().parent
if Path.cwd().name == "notebooks"
else Path.cwd().resolve()
)
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))Construct BedrockChatLLM with a Gemma 4 id. Construction is side-effect free —
the OpenAI client is only built inside .complete(), so no key is needed just to
inspect the routing. A google.gemma-4-* id gets a _mantle_delegate pointed at the
mantle base URL; a Claude id has _mantle_delegate is None and uses Converse.
from shipit_agent.llms import BedrockChatLLM, BedrockGemmaChatLLM
# Gemma 4 → routed to the OpenAI-compatible bedrock-mantle endpoint.
gemma = BedrockChatLLM(model="google.gemma-4-31b", region="us-east-1")
print("delegate type :", type(gemma._mantle_delegate).__name__)
print("delegate model:", gemma._mantle_delegate.model)
print("base_url :", gemma._mantle_delegate.client_kwargs["base_url"])
assert isinstance(gemma._mantle_delegate, BedrockGemmaChatLLM)
assert (
gemma._mantle_delegate.client_kwargs["base_url"]
== "https://bedrock-mantle.us-east-1.api.aws/openai/v1"
)delegate type : BedrockGemmaChatLLM delegate model: google.gemma-4-31b base_url : https://bedrock-mantle.us-east-1.api.aws/openai/v1
A Claude id (or any non-Gemma-4 model) has no mantle delegate — it takes the normal Bedrock Converse path. The Gemma 3 ids behave the same way (chat via Converse).
claude = BedrockChatLLM(model="bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0")
gemma3 = BedrockChatLLM(model="bedrock/google.gemma-3-27b-it")
print("claude ._mantle_delegate:", claude._mantle_delegate)
print("gemma-3 ._mantle_delegate:", gemma3._mantle_delegate)
assert claude._mantle_delegate is None
assert gemma3._mantle_delegate is None
# The region also comes from AWS_REGION_NAME / the region kwarg.
eu = BedrockChatLLM(model="google.gemma-4-26b-a4b", aws_region_name="eu-central-1")
print("eu-central-1 base_url :", eu._mantle_delegate.client_kwargs["base_url"])claude ._mantle_delegate: None gemma-3 ._mantle_delegate: None eu-central-1 base_url : https://bedrock-mantle.eu-central-1.api.aws/openai/v1
Now the real thing: a Gemma 4 agent that calls a tool. We define an add(a, b)
FunctionTool, build an Agent around BedrockChatLLM(model="google.gemma-4-31b"),
then inject a fake openai module so the mantle delegate's .complete() returns
scripted responses:
add(a=2, b=3),This is exactly the mock from tests/test_gemma_bedrock.py. Because
OpenAIChatLLM.complete() imports openai inside the method, assigning
sys.modules["openai"] before agent.run() is enough — no real endpoint is hit.
import sys
import types
from types import SimpleNamespace as ns
def install_fake_openai(*responses):
"""Inject a fake `openai` module returning the given responses in order."""
calls = {"i": 0}
class _Completions:
def create(self, **_kwargs):
# Clamp so extra turns keep returning the last (text) response —
# the agent loop always terminates cleanly.
r = responses[min(calls["i"], len(responses) - 1)]
calls["i"] += 1
return r
class _OpenAI:
def __init__(self, **_kwargs):
self.chat = ns(completions=_Completions())
fake = types.ModuleType("openai")
fake.OpenAI = _OpenAI
sys.modules["openai"] = fake
def tool_call_response(name, arguments):
msg = ns(
content="",
tool_calls=[ns(id="c1", function=ns(name=name, arguments=arguments))],
reasoning_content=None,
)
return ns(choices=[ns(message=msg)], usage=None)
def text_response(text):
msg = ns(content=text, tool_calls=[], reasoning_content=None)
return ns(choices=[ns(message=msg)], usage=None)Define the tool and the agent, script the two turns, then run.
from typing import Any
from shipit_agent import Agent, FunctionTool
ran: list[str] = []
def add(a: int, b: int, **_: Any) -> str:
"""Add two numbers."""
ran.append("add")
return str(a + b)
# Turn 1: Gemma 4 emits a native tool call. Turn 2: it answers in text.
install_fake_openai(
tool_call_response("add", '{"a": 2, "b": 3}'),
text_response("The answer is 5."),
)
agent = Agent(
llm=BedrockChatLLM(model="google.gemma-4-31b", region="us-east-1"),
tools=[FunctionTool.from_callable(add, name="add")],
auto_use_skills=False,
)
result = agent.run("What is 2 + 3?")
print("output :", result.output)
print("tool ran :", ran) # ['add'] — the tool actually executed
assert ran == ["add"]
assert "answer is 5" in result.outputoutput : The answer is 5. tool ran : ['add']
That is the complete agentic loop: model → native tool call → tool executes →
model → final answer, driven by Gemma 4 through BedrockChatLLM, with zero
credentials. The ran == ["add"] assertion proves the tool really ran (it wasn't
the model just talking about adding).
New in v1.0.15: Agent.for_role turns any of the 40+ prebuilt specialists into a runnable
agent in one line. Here the finance analyst (powered by Gemma 4) closes Q2: it calls the
new build_document tool to produce a genuine .xlsx — styled headers, frozen panes,
a live =B2+B3 formula.
We consume agent.stream(...) and render each event as it arrives with
format_event_line — the same clean tool cards Claude Code shows:
import json as _json
from shipit_agent import Agent, format_activity, format_event_line
# Script Gemma 4's two turns: a native build_document call, then the answer.
install_fake_openai(
tool_call_response("build_document", _json.dumps({
"kind": "xlsx",
"title": "Q2 Close",
"sheets": [{
"name": "P&L",
"headers": ["Item", "Amount"],
"rows": [["Revenue", 124000], ["Costs", -78500], ["Net", "=B2+B3"]],
}],
})),
text_response("Q2 close workbook is ready — net income formula included."),
)
analyst = Agent.for_role(
"finance-analyst",
llm=BedrockChatLLM(model="google.gemma-4-31b", region="us-east-1"),
)
print("role tools:", sorted(t.name for t in analyst.tools), "\n")
# ── real streaming: events render the moment they happen ──
events = []
for event in analyst.stream("Close Q2 and hand me the workbook."):
events.append(event)
line = format_event_line(event)
if line:
print(line)role tools: ['build_document', 'google_sheets', 'pdf', 'render_dashboard', 'sql', 'stripe']
⚙ build_document(kind="xlsx", title="Q2 Close", sheets=[{'name': 'P&L', 'headers': ['Item', 'A…) …⚙ build_document ✓ 272ms └ Created XLSX 'Q2 Close' → .shipit_workspace/documents/q2_close.xlsx (5,108 bytes)
# After the stream: the full run as one clean activity trace.
print(format_activity(events))⚙ build_document(kind="xlsx", title="Q2 Close", sheets=[{'name': 'P&L', 'headers': ['Item', 'A…) ✓ 272ms
└ Created XLSX 'Q2 Close' → .shipit_workspace/documents/q2_close.xlsx (5,108 bytes)
✔ run completed · 1 tool call · 2 iterations# The workbook is real — reopen it and verify the styling + live formula.
import openpyxl
done = next(e for e in events if e.type == "tool_completed")
path = done.payload["output"].split("→ ")[1].split(" (")[0]
ws = openpyxl.load_workbook(path)["P&L"]
print("header bold :", ws["A1"].font.bold)
print("frozen panes:", ws.freeze_panes)
print("live formula:", ws["B4"].value)
assert ws["B4"].value == "=B2+B3"
header bold : True frozen panes: A2 live formula: =B2+B3
Gemma 4 called a tool it had never seen, shipit executed it, and a finished spreadsheet landed on disk — with every step visible as it streamed.
The same agent can reach MCP servers by name — env vars and launchers are validated before anything starts:
from shipit_agent import connect_mcp, list_mcp_catalog
for entry in list_mcp_catalog():
need = f" (needs {', '.join(entry.required_env)})" if entry.required_env else ""
print(f"{entry.name:<14} {entry.description}{need}")
# Fail-fast demo — one clear message, not a subprocess stack trace:
import os
os.environ.pop("SLACK_BOT_TOKEN", None)
try:
connect_mcp("slack")
except ValueError as e:
print("\n", e)
# Live usage (with credentials set):
# agent = Agent.for_role(
# "finance-analyst",
# llm=BedrockChatLLM(model="google.gemma-4-31b"),
# mcps=[connect_mcp("github"), connect_mcp("filesystem", args=["."])],
# )brave-search Web search via the Brave Search API. (needs BRAVE_API_KEY)
fetch Fetch a URL and return page content as markdown.
filesystem Read/write files under the directories you pass as args.
github Repos, issues, PRs, code search on GitHub. (needs GITHUB_TOKEN)
gitlab GitLab projects, issues, and merge requests. (needs GITLAB_PERSONAL_ACCESS_TOKEN)
google-maps Places, directions, and geocoding via Google Maps. (needs GOOGLE_MAPS_API_KEY)
memory Persistent knowledge-graph memory across runs.
postgres Query PostgreSQL (read-only). Pass the connection URL as an arg.
puppeteer Headless browser: navigate, screenshot, interact with pages.
sentry Look up Sentry issues and stack traces. (needs SENTRY_AUTH_TOKEN)
slack Post and read Slack messages and channels. (needs SLACK_BOT_TOKEN, SLACK_TEAM_ID)
sqlite Query a SQLite database file passed as an arg.
MCP server 'slack' needs env var(s): SLACK_BOT_TOKEN, SLACK_TEAM_ID. Pass them via env={...} or export them.Everything below hits the real bedrock-mantle endpoint. Two ways to authenticate:
export AWS_BEARER_TOKEN_BEDROCK=... — a Bedrock API key (AWS console → Amazon Bedrock → API keys), oraws-bedrock-token-generator (pip install aws-bedrock-token-generator).If neither is available the live cells skip gracefully.
import os
LIVE = bool(os.getenv("AWS_BEARER_TOKEN_BEDROCK"))
if not LIVE:
try: # generate a short-term bearer token from standard AWS credentials
from aws_bedrock_token_generator import provide_token
os.environ["AWS_BEARER_TOKEN_BEDROCK"] = provide_token(region="us-east-1")
os.environ.setdefault("AWS_REGION_NAME", "us-east-1")
LIVE = True
print("✓ short-term Bedrock token generated from AWS credentials")
except Exception as e:
print(f"offline — no Bedrock credentials available ({type(e).__name__})")
else:
print("✓ using AWS_BEARER_TOKEN_BEDROCK from the environment")
# Fresh, unfaked OpenAI client for the live calls
sys.modules.pop("openai", None)✓ short-term Bedrock token generated from AWS credentials
<module 'openai'>
Real Gemma 4 (google.gemma-4-31b) decides to call a Python function natively — every
event rendered the instant it happens:
from shipit_agent import Agent, FunctionTool
def get_time(city: str, **_):
"""Return the local time for a city."""
return f"It's 3:00 PM in {city}."
if LIVE:
live_agent = Agent(
llm=BedrockChatLLM(model="google.gemma-4-31b", region="us-east-1"),
tools=[FunctionTool.from_callable(get_time, name="get_time")],
auto_use_skills=False,
)
for event in live_agent.stream("What time is it in Tokyo? Use the tool."):
line = format_event_line(event)
if line:
print(line)
if event.type == "run_completed":
print("→", event.payload.get("output", ""))
else:
print("skipped (offline)")⚙ get_time(city="Tokyo", _="") … ⚙ get_time ✓ 0ms └ It's 3:00 PM in Tokyo.
→ It's 3:00 PM in Tokyo.
The finance analyst role, powered by real Gemma 4, produces a genuine workbook —
then format_activity and result.summary() show exactly what happened:
if LIVE:
analyst_live = Agent.for_role(
"finance-analyst",
llm=BedrockChatLLM(model="google.gemma-4-31b", region="us-east-1"),
)
live_result = analyst_live.run(
"Use the build_document tool to create an xlsx titled 'Live Q2 Close' with one "
"sheet 'P&L': headers Item, Amount; rows Revenue 124000, Costs -78500, and a "
"Net row with a formula =B2+B3. Then tell me it's done."
)
print(format_activity(live_result))
print()
print("final :", live_result.output)
print("metrics:", live_result.summary())
else:
print("skipped (offline)")⚙ build_document(kind="xlsx", title="Live Q2 Close", sheets=[{'headers': ['Item', 'Amount'], 'na…) ✓ 13ms
└ Created XLSX 'Live Q2 Close' → .shipit_workspace/documents/live_q2_close.xlsx (5,025 bytes)
✔ run completed · 1 tool call · 2 iterations
final : It's done. I have created the 'Live Q2 Close' Excel workbook with the P&L sheet, including the revenue, costs, and the net formula.
metrics: {'duration_seconds': 6.941, 'iterations': 2, 'tool_calls': 1, 'tool_failures': 0, 'usage': {}, 'tools': {'build_document': {'calls': 1, 'failures': 0, 'total_ms': 12.9}}}connect_mcp("filesystem") launches the official MCP filesystem server over a persistent
stdio transport (via npx). Its tools join Gemma's tool set automatically:
from shipit_agent import connect_mcp
if LIVE:
fs = connect_mcp("filesystem", args=["."])
print("MCP tools:", [t.name for t in fs.discover_tools()][:7], "…\n")
mcp_agent = Agent(
llm=BedrockChatLLM(model="google.gemma-4-31b", region="us-east-1"),
mcps=[fs],
auto_use_skills=False,
)
try:
for event in mcp_agent.stream(
"Use the list_directory tool to list the current directory, "
"then name three notebooks you see."
):
line = format_event_line(event)
if line:
print(line)
if event.type == "run_completed":
print("→", event.payload.get("output", "")[:300])
finally:
fs.close()
else:
print("skipped (offline)")MCP tools: ['read_file', 'read_text_file', 'read_media_file', 'read_multiple_files', 'write_file', 'edit_file', 'create_directory'] …
⚙ list_directory(path=".") … ⚙ list_directory ✓ 7ms └ [DIR] .shipit_traces [DIR] .shipit_workspace [FILE] 01_agent_without_tools.ipynb [FILE] 02_agent_multi_tools.ipynb [FILE] 03_agent_sessions_and_history.ipynb […
→ Three notebooks in the directory are: - `01_agent_without_tools.ipynb` - `02_agent_multi_tools.ipynb` - `03_agent_sessions_and_history.ipynb`
The agent code above is provider-agnostic — Agent, FunctionTool, and
agent.run(...) never mention Gemma or Bedrock. Gemma is just a model string.
Swap the llm= and the exact same tool-using agent runs on any provider:
from shipit_agent.llms import (
BedrockChatLLM, # Gemma 4 / Gemma 3 / Claude / Nova ... on Bedrock
AnthropicChatLLM, # Claude direct
OpenAIChatLLM, # GPT
GeminiChatLLM, # Gemini
)
llm = BedrockChatLLM(model="google.gemma-4-31b") # <- Gemma on Bedrock
# llm = AnthropicChatLLM(model="claude-opus-4-1") # <- same agent, different model
agent = Agent.with_builtins(llm=llm, tools=[...])
BedrockChatLLM, covers all of Gemma on Bedrock — pass the id and it
routes: Gemma 4 → mantle (native tool use), Gemma 3 / Claude → Converse._mantle_delegate shows where a Gemma-4 id lands.openai module — the same pattern the test suite uses.