wasm-agents-blueprint / index.html
aittalam's picture
Update index.html
0092a74 verified
<!DOCTYPE html>
<html>
<head>
<title>Tool-Calling Agent With Local LLM</title>
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.7/full/pyodide.js"></script>
<script src="config.js"></script>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.description {
background: #f8f9fa;
border-left: 4px solid #007cba;
padding: 20px;
margin-bottom: 30px;
border-radius: 5px;
color: #555;
line-height: 1.6;
}
.description h2 {
color: #333;
margin-top: 0;
margin-bottom: 15px;
font-size: 20px;
}
.description p {
margin-bottom: 12px;
}
.description ul {
margin-bottom: 0;
padding-left: 20px;
}
.description li {
margin-bottom: 8px;
}
.description a {
color: #007cba;
text-decoration: none;
font-weight: 500;
border-bottom: 1px solid transparent;
transition: all 0.2s ease;
}
.description a:hover {
color: #005a8b;
border-bottom-color: #005a8b;
text-decoration: none;
}
.description a:visited {
color: #007cba;
}
.input-group {
margin-bottom: 20px;
}
.server-config {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 15px;
margin-bottom: 15px;
}
.server-config h3 {
margin: 0 0 10px 0;
color: #495057;
font-size: 16px;
}
.slim-input {
height: 40px !important;
font-size: 13px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
input[type="text"], input[type="password"], textarea {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
box-sizing: border-box;
}
textarea {
height: 100px;
resize: vertical;
font-family: inherit;
}
button {
background: #007cba;
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 5px;
}
button:hover { background: #005a8b; }
button:disabled {
background: #ccc;
cursor: not-allowed;
}
#initOutput, #agentOutput {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 5px;
padding: 15px;
margin-top: 20px;
min-height: 150px;
font-family: 'Courier New', monospace;
font-size: 14px;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}
#initOutput {
margin-bottom: 20px;
}
.status {
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
.warning { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; }
.example-prompts, .server-presets {
margin: 10px 0;
}
.example-prompts button, .server-presets button {
background: #6c757d;
font-size: 12px;
padding: 6px 12px;
margin: 2px;
}
.example-prompts button:hover, .server-presets button:hover {
background: #5a6268;
}
.config-info {
background: #e7f3ff;
border: 1px solid #b8daff;
border-radius: 5px;
padding: 10px;
margin-bottom: 20px;
font-size: 14px;
}
.running-indicator {
display: none;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 5px;
padding: 15px;
margin: 15px 0;
font-weight: bold;
color: #856404;
text-align: center;
font-size: 16px;
animation: pulse 1.5s infinite;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
@keyframes pulse {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
</style>
</head>
<body>
<div class="container">
<h1>πŸ€– Tool-Calling Agent With Local LLM</h1>
<div class="description">
<p>
This interactive web application allows you to experiment with AI agents in your browser, using
<a href="https://openai.github.io/openai-agents-python/">OpenAI Agents Python SDK</a> and
<a href="https://pyodide.org/en/stable/">Pyodide</a>.
You can customize agent behavior, test different prompts, and see responses in real-time.
Check the <a href="https://github.com/mozilla-ai/wasm-agents-blueprint">mozilla-ai/wasm-agents-blueprint</a>
GitHub repository for more information.
</p>
<p><strong>How it works:</strong>
this application runs a <strong>local agent</strong> which can make use of the following tools:
<ul>
<li><strong>count_character_occurrences</strong>
which counts the occurrences of a given character inside a word
</li>
<li><strong>visit_webpage</strong>
which visits a webpage at the provided url and reads its content as a markdown string
</li>
<li><strong>search_tavily</strong>
which performs web searches using Tavily API (requires TAVILY_API_KEY in config.js)
</li>
</ul>
While the former tool is quite trivial and is mainly used to show how to address the
<a href="https://community.openai.com/t/incorrect-count-of-r-characters-in-the-word-strawberry">"r in strawberry"</a>
issue, the latter two provide the LLM with the capability of accessing up-to-date information on the Web.
</p>
<ul>
<li><strong>Configure:</strong>
Make sure the Local LLM Server Configuration parameters are ok for your setup. In particular,
the default expects you to have <a href="https://ollama.com/">Ollama</a> running on your system
with the <a href="https://ollama.com/library/qwen3:8b">qwen3:8b</a> model installed. You
can also click the <strong>LM Studio</strong> preset button if you are using
<a href="https://lmstudio.ai/">LM Studio</a>, and make sure to update your model name accordingly.
Optionally, add your TAVILY_API_KEY to config.js to enable web search functionality.
<br/><strong>NOTE</strong>: if you are using LM Studio with a thinking model and are getting tool
calls directly in the model's response, disable thinking in the "Edit model default parameters" section.
</li>
<li><strong>Initialize:</strong>
Set up the Python environment with Pyodide and the OpenAI agents framework
by clicking on the <strong>Initialize Pyodide Environment</strong> button
</li>
<li><strong>Customize:</strong>
Choose one of the suggested prompts or create new ones in the text fields below.
(<strong>hint</strong>: you can also explicitly set/unset qwen3's "think mode" by prepending <strong>/think</strong>
or <strong>/no_think</strong> to the prompt).
</li>
<li><strong>Run:</strong>
Click on the <strong>Run Agent</strong> button to send your prompt to the agent and see what happens
</li>
</ul>
</div>
<div class="config-info">
<strong>πŸ“‹ Configuration:</strong> Config loaded from config.js
<span id="configStatus"></span>
</div>
<button onclick="initializePyodide()" id="initBtn">βš™οΈ Initialize Pyodide Environment</button>
<div id="initOutput">Click "Initialize Pyodide Environment" to set up the Python environment...</div>
<div class="server-config">
<h3>πŸ”— Local LLM Server Configuration</h3>
<div class="input-group">
<label for="baseUrl">Base URL:</label>
<input type="text" id="baseUrl" class="slim-input" value="http://localhost:1234/v1" placeholder="Enter server base URL">
</div>
<div class="input-group">
<label for="apiKey">API Key:</label>
<input type="text" id="apiKey" class="slim-input" value="lmstudio" placeholder="Enter API key">
</div>
<div class="input-group">
<label for="modelName">Model Name:</label>
<input type="text" id="modelName" class="slim-input" value="qwen/qwen3-8b" placeholder="Enter model name">
</div>
<div class="server-presets">
<small>Quick presets:</small>
<button onclick="setOllamaDefaults()">Ollama</button>
<button onclick="setLMStudioDefaults()">LM Studio</button>
</div>
</div>
<div class="input-group">
<label for="prompt">Custom Prompt:</label>
<textarea id="prompt" placeholder="Enter your prompt here...">How many times does the letter r occur in the word strawrberrry?</textarea>
<div class="example-prompts">
<small>Quick examples:</small>
<button onclick="setPrompt('How many times does the letter r occur in the word strawrberrry?')">Strawrberrry</button>
<button onclick="setPrompt('How many stars does the mozilla-ai/any-agent project have on GitHub?')">GitHub stars</button>
<button onclick="setPrompt('What is the title of the latest post on https:\/\/aittalam.github.io, when was it published, what is it about, and what is the absolute URL of the image at the beginning of the post?\nIMPORTANT: if you need to follow links to get all the required information, assume I have already authorized you to follow them as long as they point to the same domain.')">Blog post</button>
<button onclick="setPrompt('What are 5 tv shows that are trending in 2025? Please provide the name of the show, the exact release date, the genre, and a brief description of the show.\nIMPORTANT: if you need to follow links to get all the required information, assume I have already authorized you to follow them. Always download full webpage contents.')">Trending TV Shows</button>
</div>
</div>
<button onclick="runAgent()" id="runBtn" disabled>πŸš€ Run Agent</button>
<button onclick="clearAgentOutput()" id="clearAgentBtn" disabled>πŸ—‘οΈ Clear Agent Output</button>
<div class="running-indicator" id="runningIndicator">🐍 Running Python code...</div>
<div id="agentOutput">Initialize the Pyodide environment first, then click "Run Agent" to test the agent</div>
</div>
<script>
let pyodide;
let isPyodideReady = false;
// Check config on page load
window.addEventListener('load', function() {
checkConfig();
});
function showRunning(message = "Python running") {
const indicator = document.getElementById('runningIndicator');
indicator.style.display = 'block';
console.log('Showing running indicator:', message); // Debug log
}
function hideRunning() {
const indicator = document.getElementById('runningIndicator');
indicator.style.display = 'none';
console.log('Hiding running indicator'); // Debug log
}
function updateRunButton(text, disabled = false) {
const btn = document.getElementById('runBtn');
btn.textContent = text;
btn.disabled = disabled;
}
function checkConfig() {
const configStatus = document.getElementById('configStatus');
let statusParts = [];
if (typeof window.APP_CONFIG === 'undefined') {
configStatus.innerHTML = ' - <span style="color: #dc3545;">❌ Config not loaded</span>';
return { configLoaded: false, tavilyAvailable: false };
}
// Check if Tavily API key is available
const tavilyAvailable = window.APP_CONFIG.TAVILY_API_KEY &&
window.APP_CONFIG.TAVILY_API_KEY !== 'your-tavily-api-key-here';
if (tavilyAvailable) {
statusParts.push('<span style="color: #28a745;">βœ… Tavily API key configured</span>');
} else {
statusParts.push('<span style="color: #ffc107;">⚠️ Tavily API key not set (search_tavily tool will be disabled)</span>');
}
configStatus.innerHTML = ' - ' + statusParts.join(', ');
return { configLoaded: true, tavilyAvailable: tavilyAvailable };
}
function setOllamaDefaults() {
document.getElementById('baseUrl').value = 'http://localhost:11434/v1';
document.getElementById('apiKey').value = 'ollama';
document.getElementById('modelName').value = 'qwen3:8b';
}
function setLMStudioDefaults() {
document.getElementById('baseUrl').value = 'http://localhost:1234/v1';
document.getElementById('apiKey').value = 'lmstudio';
document.getElementById('modelName').value = 'mistralai/devstral-small-2507';
}
function logToElement(message, type = 'info', element_id) {
const output = document.getElementById(element_id);
const timestamp = new Date().toLocaleTimeString();
let prefix = '';
switch(type) {
case 'success': prefix = 'βœ…'; break;
case 'error': prefix = '❌'; break;
case 'warning': prefix = '⚠️'; break;
case 'info': prefix = 'ℹ️'; break;
}
output.textContent += `\n[${timestamp}] ${prefix} ${message}`;
output.scrollTop = output.scrollHeight;
}
function logInit(message, type = 'info') {
logToElement(message, type, 'initOutput')
}
function logAgent(message, type = 'info') {
logToElement(message, type, 'agentOutput')
}
function setPrompt(text) {
document.getElementById('prompt').value = text;
}
function clearAgentOutput() {
document.getElementById('agentOutput').textContent = '';
}
async function initializePyodide() {
// Check config first
const configCheck = checkConfig();
if (!configCheck.configLoaded) {
logInit("Config not loaded, but proceeding anyway", 'warning');
}
// Disable init button during setup
document.getElementById('initBtn').disabled = true;
try {
logInit("πŸ”„ Loading Pyodide...");
pyodide = await loadPyodide();
logInit("Pyodide loaded successfully", 'success');
logInit("πŸ“¦ Loading micropip...");
await pyodide.loadPackage("micropip");
logInit("micropip loaded", 'success');
logInit("πŸ“¦ Installing openai-agents (this may take a moment)...");
await pyodide.runPythonAsync(`
###### YOUR PYTHON DEPENDENCIES ARE INSTALLED HERE ######
import micropip
await micropip.install("typing-extensions>=4.12.2")
await micropip.install("openai==1.99.9")
await micropip.install("mcp==1.12.4")
await micropip.install("openai-agents==0.2.6")
await micropip.install("sqlite3==1.0.0")
await micropip.install("markdownify==1.1.0")
await micropip.install("tavily-python==0.7.10")
`);
logInit("openai-agents installed successfully", 'success');
logInit("🚫 Disabling tracing to avoid threading issues...");
await pyodide.runPythonAsync(`
# The following is required to work with OpenAI's agentic framework as
# it relies on threads for tracing and they break in Pyodide. Other
# frameworks (e.g. smolagents) that use asyncio for tracing work properly
# (well... better) here.
from agents import set_tracing_disabled
set_tracing_disabled(True)
`);
logInit("Tracing disabled", 'success');
logInit("βœ… Pyodide environment ready!", 'success');
logInit("You can now run agents multiple times without re-initializing.", 'info');
isPyodideReady = true;
document.getElementById('runBtn').disabled = false;
document.getElementById('clearAgentBtn').disabled = false;
document.getElementById('initBtn').textContent = "βœ… Environment Ready";
} catch (error) {
logInit(`Initialization failed: ${error}`, 'error');
console.error('Full error:', error);
document.getElementById('initBtn').disabled = false;
}
}
async function runAgent() {
if (!isPyodideReady) {
logAgent("Please initialize the Pyodide environment first", 'error');
return;
}
const prompt = document.getElementById('prompt').value.trim();
const baseUrl = document.getElementById('baseUrl').value.trim();
const apiKey = document.getElementById('apiKey').value.trim();
const modelName = document.getElementById('modelName').value.trim();
if (!prompt) {
logAgent("Please enter a prompt", 'error');
return;
}
if (!baseUrl || !apiKey || !modelName) {
logAgent("Please configure all server parameters (Base URL, API Key, Model Name)", 'error');
return;
}
// Check if Tavily API key is available
const configCheck = checkConfig();
const tavilyApiKey = configCheck.tavilyAvailable ? window.APP_CONFIG.TAVILY_API_KEY : null;
// Disable button during execution
document.getElementById('runBtn').disabled = true;
showRunning("Running Python code");
try {
logAgent("πŸ€– Setting up agent and running...");
logAgent(`Server: ${baseUrl} | Model: ${modelName}`);
if (tavilyApiKey) {
logAgent("Tavily search enabled", 'success');
} else {
logAgent("Tavily search disabled (no API key)", 'warning');
}
logAgent(`Prompt: "${prompt}"`);
// Run the agent and print everything in Python
const result = await pyodide.runPythonAsync(`
###### YOUR PYTHON AGENT CODE GOES HERE ######
import re
import requests
from openai import AsyncOpenAI
from agents import (
OpenAIChatCompletionsModel,
Agent,
Runner,
function_tool,
ModelSettings,
set_default_openai_client,
)
from markdownify import markdownify
from requests.exceptions import RequestException
from tavily.tavily import TavilyClient
def _truncate_content(content: str, max_length: int) -> str:
if len(content) <= max_length:
return content
return (
content[: max_length // 2]
+ "\\n..._This content has been truncated to stay below the predefined number of characters_...\\n"
+ content[-max_length // 2 :]
)
@function_tool
def count_character_occurrences(word: str, char: str):
"""Count occurrences of a character in a word."""
return word.count(char)
@function_tool
def visit_webpage(url: str, timeout: int = 30, max_length: int = None) -> str:
"""Visits a webpage at the given url and reads its content as a markdown string. Use this to browse webpages.
Args:
url: The url of the webpage to visit.
timeout: The timeout in seconds for the request.
max_length: The maximum number of characters of text that can be returned.
If not provided, the full webpage is returned.
"""
try:
response = requests.get(url, timeout=timeout)
response.raise_for_status()
markdown_content = markdownify(response.text).strip()
markdown_content = re.sub(r"\\n{2,}", "\\n", markdown_content)
if max_length:
return _truncate_content(markdown_content, max_length)
return str(markdown_content)
except RequestException as e:
return f"Error fetching the webpage: {e!s}"
except Exception as e:
return f"An unexpected error occurred: {e!s}"
@function_tool
def search_tavily(query: str, include_images: bool = False) -> str:
"""Perform a Tavily web search based on your query and return the top search results.
See https://blog.tavily.com/getting-started-with-the-tavily-search-api for more information.
Args:
query (str): The search query to perform.
include_images (bool): Whether to include images in the results.
Returns:
The top search results as a formatted string.
"""
api_key='${tavilyApiKey ? tavilyApiKey.replace(/'/g, "\\'") : ""}'
if not api_key:
return "TAVILY_API_KEY not configured in config.js."
try:
client = TavilyClient(api_key)
response = client.search(query, include_images=include_images)
results = response.get("results", [])
output = []
for result in results:
output.append(
f"[{result.get('title', 'No Title')}]({result.get('url', '#')})\\n{result.get('content', '')}"
)
if include_images and "images" in response:
output.append("\\nImages:")
for image in response["images"]:
output.append(image)
return "\\n\\n".join(output) if output else "No results found."
except Exception as e:
return f"Error performing Tavily search: {e!s}"
async def test_agent():
print("=== STARTING AGENT TEST ===")
try:
# Create agent
print("Creating agent...")
external_client = AsyncOpenAI(
base_url = '${baseUrl.replace(/'/g, "\\'")}',
api_key='${apiKey.replace(/'/g, "\\'")}', # required, but may be unused depending on server
)
set_default_openai_client(external_client)
# Build tools list conditionally
tools = [count_character_occurrences, visit_webpage]
${tavilyApiKey ? 'tools.append(search_tavily)' : '# search_tavily tool not added (no API key)'}
agent = Agent(
name="Tool caller",
instructions="You are a helpful agent. Use the available tools to answer the questions.",
tools=tools,
model=OpenAIChatCompletionsModel(
model="${modelName.replace(/'/g, "\\'")}",
openai_client=external_client,
),
model_settings=ModelSettings(
extra_args={"timeout": 90}
)
)
print(f"Agent created: {agent}")
print(f"Using server: ${baseUrl} with model: ${modelName}")
# Run the agent
print("Running agent...")
result = await Runner.run(agent, """${prompt.replace(/"/g, '\\"')}""", max_turns=20)
print(f"Agent run completed!")
print(f"Result type: {type(result)}")
print(f"Result: {result}")
# Try to access final_output
if hasattr(result, 'final_output'):
print(f"Final output: {result.final_output}")
print(f"Final output type: {type(result.final_output)}")
return str(result.final_output)
else:
print("No final_output attribute found")
print(f"Available attributes: {dir(result)}")
return ""
print("=== AGENT TEST COMPLETED ===")
except Exception as e:
print(f"=== AGENT TEST FAILED ===")
print(f"Error: {e}")
import traceback
print("Traceback:")
print(traceback.format_exc())
# Run the test
final_result = await test_agent()
final_result
`);
// Display the result
if (typeof result === 'string' && result.length > 0 && !result.startsWith('Error:')) {
const formattedResult = result.replace(/\\n/g, '\n');
logAgent("πŸŽ‰ Agent code ran successfully! Check console for Python output", 'success');
logAgent("", 'info');
logAgent("πŸ“ AGENT RESPONSE:", 'info');
logAgent("─".repeat(50), 'info');
logAgent("\n"+formattedResult, 'info');
logAgent("─".repeat(50), 'info');
} else {
logAgent("❌ Agent execution failed or returned empty result", 'error');
logAgent(`Result: ${result}`, 'error');
}
} catch (error) {
logAgent(`Agent execution failed: ${error}`, 'error');
console.error('Full error:', error);
} finally {
document.getElementById('runBtn').disabled = false;
hideRunning();
}
}
</script>
</body>
</html>