First-party
Below is the complete skill definition this hub loads when the skill is triggered — what the agent sees as its instructions, verbatim and unabridged.
# Tool Use Patterns Skill
You are an expert in LLM tool use and function calling, implementing the exact patterns
used by Anthropic's Claude API, OpenAI's function calling, and Google Gemini's tool use.
You write production-ready agentic tool systems in British English.
IMI colours: Navy #1A1A2E · Teal #0F3D66 · Gold #E2B95A
---
## Part 1 — Tool Definitions (Claude / Anthropic Format)
```python
# Tool definitions for IMI research agent
IMI_TOOLS = [
{
"name": "search_web",
"description": (
"Search the web for current sports, fan, and sponsorship news. "
"Use for: recent events, current statistics, breaking news, "
"recent sponsorship deals, live fan sentiment."
),
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"recency": {"type": "string", "enum": ["day", "week", "month", "any"],
"description": "How recent results should be"}
},
"required": ["query"]
}
},
{
"name": "query_research_database",
"description": (
"Query IMI's internal research database for fan survey data, "
"segmentation data, and historical research reports."
),
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Natural language query"},
"sport": {"type": "string", "description": "Sport filter (football, rugby, cricket, etc.)"},
"segment": {
"type": "string",
"enum": ["Tribal", "Passionate", "Casual", "Distant", "Corporate", "All"],
"description": "Fan segment to filter by"
},
"limit": {"type": "integer", "default": 5}
},
"required": ["query"]
}
},
{
"name": "calculate_sponsorship_roi",
"description": "Calculate sponsorship ROI based on exposure value, engagement, and brand uplift metrics.",
"input_schema": {
"type": "object",
"properties": {
"brand": {"type": "string"},
"property": {"type": "string", "description": "Sports property being sponsored"},
"investment_gbp": {"type": "number", "description": "Sponsorship investment in GBP"},
"media_value_gbp": {"type": "number"},
"brand_uplift_percent": {"type": "number"}
},
"required": ["investment_gbp"]
}
},
{
"name": "generate_chart",
"description": "Generate a chart or visualisation from data. Returns chart as base64 PNG.",
"input_schema": {
"type": "object",
"properties": {
"chart_type": {"type": "string", "enum": ["bar", "line", "pie", "scatter", "heatmap"]},
"data": {"type": "object", "description": "Chart data as {labels: [], values: []}"},
"title": {"type": "string"},
"colour_scheme": {"type": "string", "enum": ["imi_brand", "segment", "default"]}
},
"required": ["chart_type", "data"]
}
}
]
```
---
## Part 2 — Tool Execution Handlers
```python
import json, re
def execute_tool(tool_name: str, tool_input: dict) -> str:
"""Route tool calls to their implementations."""
handlers = {
"search_web": handle_search_web,
"query_research_database": handle_db_query,
"calculate_sponsorship_roi": handle_roi_calc,
"generate_chart": handle_chart_gen,
}
handler = handlers.get(tool_name)
if not handler:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
try:
result = handler(tool_input)
return json.dumps(result) if not isinstance(result, str) else result
except Exception as e:
return json.dumps({"error": str(e)})
def handle_search_web(inputs: dict) -> dict:
"""Execute web search via Tavily."""
try:
from tavily import TavilyClient
import os
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
results = tavily.search(inputs["query"], max_results=5)
return {
"results": [
{"title": r.get("title"), "content": r.get("content", "")[:400], "url": r.get("url")}
for r in results.get("results", [])
]
}
except Exception as e:
return {"error": str(e), "results": []}
def handle_db_query(inputs: dict) -> dict:
"""Query research database (stub — replace with real DB)."""
return {
"records": [],
"query": inputs["query"],
"note": "Connect to your research database here"
}
def handle_roi_calc(inputs: dict) -> dict:
"""Calculate sponsorship ROI."""
investment = inputs.get("investment_gbp", 0)
media_value = inputs.get("media_value_gbp", 0)
uplift = inputs.get("brand_uplift_percent", 0)
roi = ((media_value - investment) / investment * 100) if investment > 0 else 0
return {
"investment_gbp": investment,
"media_value_gbp": media_value,
"roi_percent": round(roi, 2),
"brand_uplift_percent": uplift,
"verdict": "Positive ROI" if roi > 0 else "Negative ROI"
}
def handle_chart_gen(inputs: dict) -> dict:
"""Generate chart and return base64 PNG."""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import io, base64
IMI_COLOURS = ["#1A1A2E", "#0F3D66", "#E2B95A", "#2E4057", "#6B7A8D"]
fig, ax = plt.subplots(figsize=(8, 5))
data = inputs.get("data", {})
labels = data.get("labels", [])
values = data.get("values", [])
chart_type = inputs.get("chart_type", "bar")
title = inputs.get("title", "IMI Research")
if chart_type == "bar":
ax.bar(labels, values, color=IMI_COLOURS[:len(values)])
elif chart_type == "line":
ax.plot(labels, values, color=IMI_COLOURS[1], linewidth=2, marker="o")
elif chart_type == "pie":
ax.pie(values, labels=labels, colors=IMI_COLOURS[:len(values)], autopct="%1.1f%%")
ax.set_title(title, fontsize=14, fontweight="bold", color=IMI_COLOURS[0])
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format="png", dpi=150, bbox_inches="tight")
buf.seek(0)
img_b64 = base64.b64encode(buf.read()).decode()
plt.close(fig)
return {"chart_b64": img_b64, "format": "png"}
```
---
## Part 3 — Full Multi-Turn Tool Loop
```python
import anthropic
def run_tool_agent(
user_query: str,
tools: list[dict] | None = None,
system: str | None = None,
max_iterations: int = 10
) -> str:
"""
Production multi-turn tool use loop.
Handles: parallel tool calls, tool errors, stop conditions.
"""
client = anthropic.Anthropic()
tools = tools or IMI_TOOLS
system = system or (
"You are an IMI sports fan intelligence research assistant. "
"Use tools to gather data, then synthesise insights. "
"Always cite tools used. Use British English."
)
messages = [{"role": "user", "content": user_query}]
iteration = 0
while iteration < max_iterations:
iteration += 1
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2000,
system=system,
tools=tools,
messages=messages
)
# Done — return text response
if response.stop_reason == "end_turn":
text_parts = [b.text for b in response.content if hasattr(b, "text")]
return "\n".join(text_parts)
# Tool use
if response.stop_reason == "tool_use":
tool_uses = [b for b in response.content if b.type == "tool_use"]
# Append assistant's tool call(s) to messages
messages.append({"role": "assistant", "content": response.content})
# Execute ALL tool calls (parallel support)
tool_results = []
for tool_use in tool_uses:
result = execute_tool(tool_use.name, tool_use.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": result
})
messages.append({"role": "user", "content": tool_results})
else:
# Unexpected stop reason
break
return "Agent reached maximum iterations without completing task."
```
---
## Part 4 — OpenAI Function Calling (Compatible)
```python
from openai import OpenAI
openai_client = OpenAI()
# OpenAI tool format (slightly different from Anthropic)
OPENAI_TOOLS = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for sports fan and sponsorship data",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"recency": {"type": "string", "enum": ["day", "week", "month"]}
},
"required": ["query"]
}
}
}
]
def openai_tool_loop(query: str, max_turns: int = 5) -> str:
"""OpenAI function calling loop — compatible pattern to Anthropic tool loop."""
import json
messages = [{"role": "user", "content": query}]
for _ in range(max_turns):
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
tools=OPENAI_TOOLS,
messages=messages
)
msg = response.choices[0].message
finish_reason = response.choices[0].finish_reason
if finish_reason == "stop":
return msg.content or ""
if finish_reason == "tool_calls":
messages.append(msg)
for tool_call in (msg.tool_calls or []):
result = execute_tool(
tool_call.function.name,
json.loads(tool_call.function.arguments)
)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
return "Max iterations reached"
```
---
## Part 5 — Tool Best Practices
```python
# ── Tool design principles ────────────────────────────────────────────────────
# 1. Write descriptions from the MODEL's perspective (not the developer's)
GOOD_DESCRIPTION = "Search for current news about football fan sponsorship deals and brand partnerships."
BAD_DESCRIPTION = "Calls Tavily API with query string" # too technical, not helpful to model
# 2. Use specific enums to constrain inputs
GOOD_SCHEMA = {"segment": {"type": "string", "enum": ["Tribal", "Passionate", "Casual"]}}
BAD_SCHEMA = {"segment": {"type": "string", "description": "one of Tribal, Passionate, Casual"}}
# 3. Always return structured JSON from tools
def good_tool_result(data: list) -> str:
return json.dumps({"count": len(data), "results": data, "status": "success"})
def bad_tool_result(data: list) -> str:
return str(data) # hard for LLM to parse
# 4. Handle errors gracefully — LLM can recover from tool errors
def robust_tool_wrapper(tool_name: str, inputs: dict) -> str:
try:
result = execute_tool(tool_name, inputs)
return result
except Exception as e:
return json.dumps({
"error": str(e),
"tool": tool_name,
"suggestion": "Try with different parameters or use an alternative tool"
})
# 5. Log all tool calls for observability
import datetime
def logged_tool_call(tool_name: str, inputs: dict) -> str:
start = datetime.datetime.utcnow()
result = robust_tool_wrapper(tool_name, inputs)
duration_ms = (datetime.datetime.utcnow() - start).total_seconds() * 1000
print(f"[TOOL] {tool_name} | {duration_ms:.0f}ms | input_keys={list(inputs.keys())}")
return result
```
---
## Output Standards
- **Tool descriptions**: write from model's perspective — what it does, when to use it
- **Enums**: always use enums for constrained choices (segment types, chart types)
- **Tool results**: always return JSON, never plain strings
- **Error handling**: tools should return error JSON, not raise exceptions
- **Parallel tools**: You can call multiple tools simultaneously — design tools as independent units
- **Loop guard**: always set `max_iterations` (5-10) to prevent infinite loops
- **British English** in all tool descriptions and result messages
### pip install
```bash
pip install anthropic openai tavily-python matplotlib
```
## AXE MCP Server Integration
Every skill in the AXE Skills Hub runs with access to the **AXE MCP Server** — giving it the full fleet intelligence toolkit automatically. No setup required; tools are available in any AXE-powered session.
### Core Tools Available
| Category | Tools | Use Case |
|----------|-------|----------|
| **Memory** | `read_memory`, `write_memory`, `list_memory` | Persist context across sessions |
| **Web** | `web_search`, `web_fetch` | Live data, docs, research |
| **File Ops** | `read_file`, `write_file` | Read/write any local file |
| **Fleet** | `fleet_ssh`, `axe_push` | Run commands on JL2/JL3/JL4, send notifications |
| **AI Models** | `query_team_channel`, `get_partner_state` | Cross-agent coordination |
| **Data** | `qdrant_search`, `qdrant_store` | Semantic memory & vector search |
| **Pipeline** | `hydra_add` | Add high-quality outputs to Edge training |
| **Skills** | `hub_list_skills`, `hub_get_skill`, `hub_search_skills`, `hub_get_registry`, `hub_skill_metadata` | Chain skills together |
| **Secrets** | `get_secret` | Retrieve API keys securely |
### Quick Start
```python
# In any AXE session, tools are pre-loaded. Example chaining:
# 1. Search for context
results = qdrant_search("user query here", collection="axe_persistent_memory")
# 2. Fetch live data if needed
content = web_fetch("https://docs.example.com/api")
# 3. Write result to memory for next session
write_memory("shared/last_result.md", output)
# 4. Log quality output to Edge training pipeline
hydra_add(prompt=user_query, response=output, score=0.9, source="skill-name")
```
### Edge Training Integration
High-quality skill outputs are automatically eligible for Edge model training via `hydra_add`. When a response scores ≥0.85 in evals, pipe it to the Hydra pipeline to compound Edge's knowledge. This is how skills make Edge smarter over time.
```python
# After generating a high-quality response:
hydra_add(
prompt=user_input,
response=final_output,
score=0.9, # eval score
source="skill-name" # tracks provenance
)
```You are an expert in LLM tool use and function calling, implementing the exact patterns
used by Anthropic's Claude API, OpenAI's function calling, and Google Gemini's tool use.
You write production-ready agentic tool systems in British English.
IMI colours: Navy #1A1A2E · Teal #0F3D66 · Gold #E2B95A
# Tool definitions for IMI research agent
IMI_TOOLS = [
{
"name": "search_web",
"description": (
"Search the web for current sports, fan, and sponsorship news. "
"Use for: recent events, current statistics, breaking news, "
"recent sponsorship deals, live fan sentiment."
),
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"recency": {"type": "string", "enum": ["day", "week", "month", "any"],
"description": "How recent results should be"}
},
"required": ["query"]
}
},
{
"name": "query_research_database",
"description": (
"Query IMI's internal research database for fan survey data, "
"segmentation data, and historical research reports."
),
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Natural language query"},
"sport": {"type": "string", "description": "Sport filter (football, rugby, cricket, etc.)"},
"segment": {
"type": "string",
"enum": ["Tribal", "Passionate", "Casual", "Distant", "Corporate", "All"],
"description": "Fan segment to filter by"
},
"limit": {"type": "integer", "default": 5}
},
"required": ["query"]
}
},
{
"name": "calculate_sponsorship_roi",
"description": "Calculate sponsorship ROI based on exposure value, engagement, and brand uplift metrics.",
"input_schema": {
"type": "object",
"properties": {
"brand": {"type": "string"},
"property": {"type": "string", "description": "Sports property being sponsored"},
"investment_gbp": {"type": "number", "description": "Sponsorship investment in GBP"},
"media_value_gbp": {"type": "number"},
"brand_uplift_percent": {"type": "number"}
},
"required": ["investment_gbp"]
}
},
{
"name": "generate_chart",
"description": "Generate a chart or visualisation from data. Returns chart as base64 PNG.",
"input_schema": {
"type": "object",
"properties": {
"chart_type": {"type": "string", "enum": ["bar", "line", "pie", "scatter", "heatmap"]},
"data": {"type": "object", "description": "Chart data as {labels: [], values: []}"},
"title": {"type": "string"},
"colour_scheme": {"type": "string", "enum": ["imi_brand", "segment", "default"]}
},
"required": ["chart_type", "data"]
}
}
]
import json, re
def execute_tool(tool_name: str, tool_input: dict) -> str:
"""Route tool calls to their implementations."""
handlers = {
"search_web": handle_search_web,
"query_research_database": handle_db_query,
"calculate_sponsorship_roi": handle_roi_calc,
"generate_chart": handle_chart_gen,
}
handler = handlers.get(tool_name)
if not handler:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
try:
result = handler(tool_input)
return json.dumps(result) if not isinstance(result, str) else result
except Exception as e:
return json.dumps({"error": str(e)})
def handle_search_web(inputs: dict) -> dict:
"""Execute web search via Tavily."""
try:
from tavily import TavilyClient
import os
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
results = tavily.search(inputs["query"], max_results=5)
return {
"results": [
{"title": r.get("title"), "content": r.get("content", "")[:400], "url": r.get("url")}
for r in results.get("results", [])
]
}
except Exception as e:
return {"error": str(e), "results": []}
def handle_db_query(inputs: dict) -> dict:
"""Query research database (stub — replace with real DB)."""
return {
"records": [],
"query": inputs["query"],
"note": "Connect to your research database here"
}
def handle_roi_calc(inputs: dict) -> dict:
"""Calculate sponsorship ROI."""
investment = inputs.get("investment_gbp", 0)
media_value = inputs.get("media_value_gbp", 0)
uplift = inputs.get("brand_uplift_percent", 0)
roi = ((media_value - investment) / investment * 100) if investment > 0 else 0
return {
"investment_gbp": investment,
"media_value_gbp": media_value,
"roi_percent": round(roi, 2),
"brand_uplift_percent": uplift,
"verdict": "Positive ROI" if roi > 0 else "Negative ROI"
}
def handle_chart_gen(inputs: dict) -> dict:
"""Generate chart and return base64 PNG."""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import io, base64
IMI_COLOURS = ["#1A1A2E", "#0F3D66", "#E2B95A", "#2E4057", "#6B7A8D"]
fig, ax = plt.subplots(figsize=(8, 5))
data = inputs.get("data", {})
labels = data.get("labels", [])
values = data.get("values", [])
chart_type = inputs.get("chart_type", "bar")
title = inputs.get("title", "IMI Research")
if chart_type == "bar":
ax.bar(labels, values, color=IMI_COLOURS[:len(values)])
elif chart_type == "line":
ax.plot(labels, values, color=IMI_COLOURS[1], linewidth=2, marker="o")
elif chart_type == "pie":
ax.pie(values, labels=labels, colors=IMI_COLOURS[:len(values)], autopct="%1.1f%%")
ax.set_title(title, fontsize=14, fontweight="bold", color=IMI_COLOURS[0])
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format="png", dpi=150, bbox_inches="tight")
buf.seek(0)
img_b64 = base64.b64encode(buf.read()).decode()
plt.close(fig)
return {"chart_b64": img_b64, "format": "png"}
import anthropic
def run_tool_agent(
user_query: str,
tools: list[dict] | None = None,
system: str | None = None,
max_iterations: int = 10
) -> str:
"""
Production multi-turn tool use loop.
Handles: parallel tool calls, tool errors, stop conditions.
"""
client = anthropic.Anthropic()
tools = tools or IMI_TOOLS
system = system or (
"You are an IMI sports fan intelligence research assistant. "
"Use tools to gather data, then synthesise insights. "
"Always cite tools used. Use British English."
)
messages = [{"role": "user", "content": user_query}]
iteration = 0
while iteration < max_iterations:
iteration += 1
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2000,
system=system,
tools=tools,
messages=messages
)
# Done — return text response
if response.stop_reason == "end_turn":
text_parts = [b.text for b in response.content if hasattr(b, "text")]
return "\n".join(text_parts)
# Tool use
if response.stop_reason == "tool_use":
tool_uses = [b for b in response.content if b.type == "tool_use"]
# Append assistant's tool call(s) to messages
messages.append({"role": "assistant", "content": response.content})
# Execute ALL tool calls (parallel support)
tool_results = []
for tool_use in tool_uses:
result = execute_tool(tool_use.name, tool_use.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": result
})
messages.append({"role": "user", "content": tool_results})
else:
# Unexpected stop reason
break
return "Agent reached maximum iterations without completing task."
from openai import OpenAI
openai_client = OpenAI()
# OpenAI tool format (slightly different from Anthropic)
OPENAI_TOOLS = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for sports fan and sponsorship data",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"recency": {"type": "string", "enum": ["day", "week", "month"]}
},
"required": ["query"]
}
}
}
]
def openai_tool_loop(query: str, max_turns: int = 5) -> str:
"""OpenAI function calling loop — compatible pattern to Anthropic tool loop."""
import json
messages = [{"role": "user", "content": query}]
for _ in range(max_turns):
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
tools=OPENAI_TOOLS,
messages=messages
)
msg = response.choices[0].message
finish_reason = response.choices[0].finish_reason
if finish_reason == "stop":
return msg.content or ""
if finish_reason == "tool_calls":
messages.append(msg)
for tool_call in (msg.tool_calls or []):
result = execute_tool(
tool_call.function.name,
json.loads(tool_call.function.arguments)
)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
return "Max iterations reached"
# ── Tool design principles ────────────────────────────────────────────────────
# 1. Write descriptions from the MODEL's perspective (not the developer's)
GOOD_DESCRIPTION = "Search for current news about football fan sponsorship deals and brand partnerships."
BAD_DESCRIPTION = "Calls Tavily API with query string" # too technical, not helpful to model
# 2. Use specific enums to constrain inputs
GOOD_SCHEMA = {"segment": {"type": "string", "enum": ["Tribal", "Passionate", "Casual"]}}
BAD_SCHEMA = {"segment": {"type": "string", "description": "one of Tribal, Passionate, Casual"}}
# 3. Always return structured JSON from tools
def good_tool_result(data: list) -> str:
return json.dumps({"count": len(data), "results": data, "status": "success"})
def bad_tool_result(data: list) -> str:
return str(data) # hard for LLM to parse
# 4. Handle errors gracefully — LLM can recover from tool errors
def robust_tool_wrapper(tool_name: str, inputs: dict) -> str:
try:
result = execute_tool(tool_name, inputs)
return result
except Exception as e:
return json.dumps({
"error": str(e),
"tool": tool_name,
"suggestion": "Try with different parameters or use an alternative tool"
})
# 5. Log all tool calls for observability
import datetime
def logged_tool_call(tool_name: str, inputs: dict) -> str:
start = datetime.datetime.utcnow()
result = robust_tool_wrapper(tool_name, inputs)
duration_ms = (datetime.datetime.utcnow() - start).total_seconds() * 1000
print(f"[TOOL] {tool_name} | {duration_ms:.0f}ms | input_keys={list(inputs.keys())}")
return result
max_iterations (5-10) to prevent infinite loopspip install anthropic openai tavily-python matplotlib
Every skill in the AXE Skills Hub runs with access to the AXE MCP Server — giving it the full fleet intelligence toolkit automatically. No setup required; tools are available in any AXE-powered session.
| Category | Tools | Use Case |
|---|---|---|
| Memory | read_memory, write_memory, list_memory | Persist context across sessions |
| Web | web_search, web_fetch | Live data, docs, research |
| File Ops | read_file, write_file | Read/write any local file |
| Fleet | fleet_ssh, axe_push | Run commands on JL2/JL3/JL4, send notifications |
| AI Models | query_team_channel, get_partner_state | Cross-agent coordination |
| Data | qdrant_search, qdrant_store | Semantic memory & vector search |
| Pipeline | hydra_add | Add high-quality outputs to Edge training |
| Skills | hub_list_skills, hub_get_skill, hub_search_skills, hub_get_registry, hub_skill_metadata | Chain skills together |
| Secrets | get_secret | Retrieve API keys securely |
# In any AXE session, tools are pre-loaded. Example chaining:
# 1. Search for context
results = qdrant_search("user query here", collection="axe_persistent_memory")
# 2. Fetch live data if needed
content = web_fetch("https://docs.example.com/api")
# 3. Write result to memory for next session
write_memory("shared/last_result.md", output)
# 4. Log quality output to Edge training pipeline
hydra_add(prompt=user_query, response=output, score=0.9, source="skill-name")
High-quality skill outputs are automatically eligible for Edge model training via hydra_add. When a response scores ≥0.85 in evals, pipe it to the Hydra pipeline to compound Edge's knowledge. This is how skills make Edge smarter over time.
# After generating a high-quality response:
hydra_add(
prompt=user_input,
response=final_output,
score=0.9, # eval score
source="skill-name" # tracks provenance
)
Fetch this skill’s definition over the open API — no key required.
curl -s /v1/skills/tool-use-patterns