Update index.html
Browse files- index.html +133 -15
index.html
CHANGED
@@ -3,6 +3,7 @@
|
|
3 |
<head>
|
4 |
<title>Tool-Calling Agent With Local LLM</title>
|
5 |
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.7/full/pyodide.js"></script>
|
|
|
6 |
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
7 |
<meta content="utf-8" http-equiv="encoding">
|
8 |
<style>
|
@@ -159,6 +160,15 @@
|
|
159 |
background: #5a6268;
|
160 |
}
|
161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
.running-indicator {
|
163 |
display: none;
|
164 |
background: #fff3cd;
|
@@ -203,16 +213,24 @@
|
|
203 |
<li><strong>visit_webpage</strong>
|
204 |
which visits a webpage at the provided url and reads its content as a markdown string
|
205 |
</li>
|
|
|
|
|
|
|
206 |
</ul>
|
207 |
While the former tool is quite trivial and is mainly used to show how to address the
|
208 |
<a href="https://community.openai.com/t/incorrect-count-of-r-characters-in-the-word-strawberry">"r in strawberry"</a>
|
209 |
-
issue, the latter
|
210 |
</p>
|
211 |
<ul>
|
212 |
<li><strong>Configure:</strong>
|
213 |
Make sure the Local LLM Server Configuration parameters are ok for your setup. In particular,
|
214 |
the default expects you to have <a href="https://ollama.com/">Ollama</a> running on your system
|
215 |
-
with the <a href="https://ollama.com/library/qwen3:8b">qwen3:8b</a> model installed.
|
|
|
|
|
|
|
|
|
|
|
216 |
</li>
|
217 |
<li><strong>Initialize:</strong>
|
218 |
Set up the Python environment with Pyodide and the OpenAI agents framework
|
@@ -229,6 +247,11 @@
|
|
229 |
</ul>
|
230 |
</div>
|
231 |
|
|
|
|
|
|
|
|
|
|
|
232 |
<button onclick="initializePyodide()" id="initBtn">⚙️ Initialize Pyodide Environment</button>
|
233 |
|
234 |
<div id="initOutput">Click "Initialize Pyodide Environment" to set up the Python environment...</div>
|
@@ -237,15 +260,15 @@
|
|
237 |
<h3>🔗 Local LLM Server Configuration</h3>
|
238 |
<div class="input-group">
|
239 |
<label for="baseUrl">Base URL:</label>
|
240 |
-
<input type="text" id="baseUrl" class="slim-input" value="http://localhost:
|
241 |
</div>
|
242 |
<div class="input-group">
|
243 |
<label for="apiKey">API Key:</label>
|
244 |
-
<input type="text" id="apiKey" class="slim-input" value="
|
245 |
</div>
|
246 |
<div class="input-group">
|
247 |
<label for="modelName">Model Name:</label>
|
248 |
-
<input type="text" id="modelName" class="slim-input" value="qwen3
|
249 |
</div>
|
250 |
|
251 |
<div class="server-presets">
|
@@ -263,7 +286,8 @@
|
|
263 |
<small>Quick examples:</small>
|
264 |
<button onclick="setPrompt('How many times does the letter r occur in the word strawrberrry?')">Strawrberrry</button>
|
265 |
<button onclick="setPrompt('How many stars does the mozilla-ai/any-agent project have on GitHub?')">GitHub stars</button>
|
266 |
-
<button onclick="setPrompt('What is the title of the latest post on 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>
|
|
|
267 |
</div>
|
268 |
</div>
|
269 |
|
@@ -279,6 +303,11 @@
|
|
279 |
let pyodide;
|
280 |
let isPyodideReady = false;
|
281 |
|
|
|
|
|
|
|
|
|
|
|
282 |
function showRunning(message = "Python running") {
|
283 |
const indicator = document.getElementById('runningIndicator');
|
284 |
indicator.style.display = 'block';
|
@@ -297,6 +326,29 @@
|
|
297 |
btn.disabled = disabled;
|
298 |
}
|
299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
300 |
function setOllamaDefaults() {
|
301 |
document.getElementById('baseUrl').value = 'http://localhost:11434/v1';
|
302 |
document.getElementById('apiKey').value = 'ollama';
|
@@ -306,10 +358,9 @@
|
|
306 |
function setLMStudioDefaults() {
|
307 |
document.getElementById('baseUrl').value = 'http://localhost:1234/v1';
|
308 |
document.getElementById('apiKey').value = 'lmstudio';
|
309 |
-
document.getElementById('modelName').value = '
|
310 |
}
|
311 |
|
312 |
-
|
313 |
function logToElement(message, type = 'info', element_id) {
|
314 |
const output = document.getElementById(element_id);
|
315 |
const timestamp = new Date().toLocaleTimeString();
|
@@ -343,6 +394,12 @@
|
|
343 |
}
|
344 |
|
345 |
async function initializePyodide() {
|
|
|
|
|
|
|
|
|
|
|
|
|
346 |
// Disable init button during setup
|
347 |
document.getElementById('initBtn').disabled = true;
|
348 |
|
@@ -360,10 +417,13 @@
|
|
360 |
###### YOUR PYTHON DEPENDENCIES ARE INSTALLED HERE ######
|
361 |
|
362 |
import micropip
|
363 |
-
await micropip.install("
|
|
|
|
|
|
|
364 |
await micropip.install("sqlite3==1.0.0")
|
365 |
-
await micropip.install("requests==2.31.0")
|
366 |
await micropip.install("markdownify==1.1.0")
|
|
|
367 |
`);
|
368 |
logInit("openai-agents installed successfully", 'success');
|
369 |
|
@@ -415,6 +475,10 @@ set_tracing_disabled(True)
|
|
415 |
return;
|
416 |
}
|
417 |
|
|
|
|
|
|
|
|
|
418 |
// Disable button during execution
|
419 |
document.getElementById('runBtn').disabled = true;
|
420 |
showRunning("Running Python code");
|
@@ -422,6 +486,11 @@ set_tracing_disabled(True)
|
|
422 |
try {
|
423 |
logAgent("🤖 Setting up agent and running...");
|
424 |
logAgent(`Server: ${baseUrl} | Model: ${modelName}`);
|
|
|
|
|
|
|
|
|
|
|
425 |
logAgent(`Prompt: "${prompt}"`);
|
426 |
|
427 |
// Run the agent and print everything in Python
|
@@ -441,6 +510,7 @@ from agents import (
|
|
441 |
)
|
442 |
from markdownify import markdownify
|
443 |
from requests.exceptions import RequestException
|
|
|
444 |
|
445 |
def _truncate_content(content: str, max_length: int) -> str:
|
446 |
if len(content) <= max_length:
|
@@ -457,27 +527,71 @@ def count_character_occurrences(word: str, char: str):
|
|
457 |
return word.count(char)
|
458 |
|
459 |
@function_tool
|
460 |
-
def visit_webpage(url: str) -> str:
|
461 |
"""Visits a webpage at the given url and reads its content as a markdown string. Use this to browse webpages.
|
462 |
|
463 |
Args:
|
464 |
url: The url of the webpage to visit.
|
|
|
|
|
|
|
465 |
|
466 |
"""
|
467 |
try:
|
468 |
-
response = requests.get(url)
|
469 |
response.raise_for_status()
|
470 |
|
471 |
-
markdown_content = markdownify(response.text).strip()
|
472 |
|
473 |
markdown_content = re.sub(r"\\n{2,}", "\\n", markdown_content)
|
474 |
|
475 |
-
|
|
|
|
|
|
|
|
|
476 |
except RequestException as e:
|
477 |
return f"Error fetching the webpage: {e!s}"
|
478 |
except Exception as e:
|
479 |
return f"An unexpected error occurred: {e!s}"
|
480 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
481 |
async def test_agent():
|
482 |
print("=== STARTING AGENT TEST ===")
|
483 |
try:
|
@@ -489,10 +603,14 @@ async def test_agent():
|
|
489 |
)
|
490 |
set_default_openai_client(external_client)
|
491 |
|
|
|
|
|
|
|
|
|
492 |
agent = Agent(
|
493 |
name="Tool caller",
|
494 |
instructions="You are a helpful agent. Use the available tools to answer the questions.",
|
495 |
-
tools=
|
496 |
model=OpenAIChatCompletionsModel(
|
497 |
model="${modelName.replace(/'/g, "\\'")}",
|
498 |
openai_client=external_client,
|
|
|
3 |
<head>
|
4 |
<title>Tool-Calling Agent With Local LLM</title>
|
5 |
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.7/full/pyodide.js"></script>
|
6 |
+
<script src="config.js"></script>
|
7 |
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
8 |
<meta content="utf-8" http-equiv="encoding">
|
9 |
<style>
|
|
|
160 |
background: #5a6268;
|
161 |
}
|
162 |
|
163 |
+
.config-info {
|
164 |
+
background: #e7f3ff;
|
165 |
+
border: 1px solid #b8daff;
|
166 |
+
border-radius: 5px;
|
167 |
+
padding: 10px;
|
168 |
+
margin-bottom: 20px;
|
169 |
+
font-size: 14px;
|
170 |
+
}
|
171 |
+
|
172 |
.running-indicator {
|
173 |
display: none;
|
174 |
background: #fff3cd;
|
|
|
213 |
<li><strong>visit_webpage</strong>
|
214 |
which visits a webpage at the provided url and reads its content as a markdown string
|
215 |
</li>
|
216 |
+
<li><strong>search_tavily</strong>
|
217 |
+
which performs web searches using Tavily API (requires TAVILY_API_KEY in config.js)
|
218 |
+
</li>
|
219 |
</ul>
|
220 |
While the former tool is quite trivial and is mainly used to show how to address the
|
221 |
<a href="https://community.openai.com/t/incorrect-count-of-r-characters-in-the-word-strawberry">"r in strawberry"</a>
|
222 |
+
issue, the latter two provide the LLM with the capability of accessing up-to-date information on the Web.
|
223 |
</p>
|
224 |
<ul>
|
225 |
<li><strong>Configure:</strong>
|
226 |
Make sure the Local LLM Server Configuration parameters are ok for your setup. In particular,
|
227 |
the default expects you to have <a href="https://ollama.com/">Ollama</a> running on your system
|
228 |
+
with the <a href="https://ollama.com/library/qwen3:8b">qwen3:8b</a> model installed. You
|
229 |
+
can also click the <strong>LM Studio</strong> preset button if you are using
|
230 |
+
<a href="https://lmstudio.ai/">LM Studio</a>, and make sure to update your model name accordingly.
|
231 |
+
Optionally, add your TAVILY_API_KEY to config.js to enable web search functionality.
|
232 |
+
<br/><strong>NOTE</strong>: if you are using LM Studio with a thinking model and are getting tool
|
233 |
+
calls directly in the model's response, disable thinking in the "Edit model default parameters" section.
|
234 |
</li>
|
235 |
<li><strong>Initialize:</strong>
|
236 |
Set up the Python environment with Pyodide and the OpenAI agents framework
|
|
|
247 |
</ul>
|
248 |
</div>
|
249 |
|
250 |
+
<div class="config-info">
|
251 |
+
<strong>📋 Configuration:</strong> Config loaded from config.js
|
252 |
+
<span id="configStatus"></span>
|
253 |
+
</div>
|
254 |
+
|
255 |
<button onclick="initializePyodide()" id="initBtn">⚙️ Initialize Pyodide Environment</button>
|
256 |
|
257 |
<div id="initOutput">Click "Initialize Pyodide Environment" to set up the Python environment...</div>
|
|
|
260 |
<h3>🔗 Local LLM Server Configuration</h3>
|
261 |
<div class="input-group">
|
262 |
<label for="baseUrl">Base URL:</label>
|
263 |
+
<input type="text" id="baseUrl" class="slim-input" value="http://localhost:1234/v1" placeholder="Enter server base URL">
|
264 |
</div>
|
265 |
<div class="input-group">
|
266 |
<label for="apiKey">API Key:</label>
|
267 |
+
<input type="text" id="apiKey" class="slim-input" value="lmstudio" placeholder="Enter API key">
|
268 |
</div>
|
269 |
<div class="input-group">
|
270 |
<label for="modelName">Model Name:</label>
|
271 |
+
<input type="text" id="modelName" class="slim-input" value="qwen/qwen3-8b" placeholder="Enter model name">
|
272 |
</div>
|
273 |
|
274 |
<div class="server-presets">
|
|
|
286 |
<small>Quick examples:</small>
|
287 |
<button onclick="setPrompt('How many times does the letter r occur in the word strawrberrry?')">Strawrberrry</button>
|
288 |
<button onclick="setPrompt('How many stars does the mozilla-ai/any-agent project have on GitHub?')">GitHub stars</button>
|
289 |
+
<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>
|
290 |
+
<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>
|
291 |
</div>
|
292 |
</div>
|
293 |
|
|
|
303 |
let pyodide;
|
304 |
let isPyodideReady = false;
|
305 |
|
306 |
+
// Check config on page load
|
307 |
+
window.addEventListener('load', function() {
|
308 |
+
checkConfig();
|
309 |
+
});
|
310 |
+
|
311 |
function showRunning(message = "Python running") {
|
312 |
const indicator = document.getElementById('runningIndicator');
|
313 |
indicator.style.display = 'block';
|
|
|
326 |
btn.disabled = disabled;
|
327 |
}
|
328 |
|
329 |
+
function checkConfig() {
|
330 |
+
const configStatus = document.getElementById('configStatus');
|
331 |
+
let statusParts = [];
|
332 |
+
|
333 |
+
if (typeof window.APP_CONFIG === 'undefined') {
|
334 |
+
configStatus.innerHTML = ' - <span style="color: #dc3545;">❌ Config not loaded</span>';
|
335 |
+
return { configLoaded: false, tavilyAvailable: false };
|
336 |
+
}
|
337 |
+
|
338 |
+
// Check if Tavily API key is available
|
339 |
+
const tavilyAvailable = window.APP_CONFIG.TAVILY_API_KEY &&
|
340 |
+
window.APP_CONFIG.TAVILY_API_KEY !== 'your-tavily-api-key-here';
|
341 |
+
|
342 |
+
if (tavilyAvailable) {
|
343 |
+
statusParts.push('<span style="color: #28a745;">✅ Tavily API key configured</span>');
|
344 |
+
} else {
|
345 |
+
statusParts.push('<span style="color: #ffc107;">⚠️ Tavily API key not set (search_tavily tool will be disabled)</span>');
|
346 |
+
}
|
347 |
+
|
348 |
+
configStatus.innerHTML = ' - ' + statusParts.join(', ');
|
349 |
+
return { configLoaded: true, tavilyAvailable: tavilyAvailable };
|
350 |
+
}
|
351 |
+
|
352 |
function setOllamaDefaults() {
|
353 |
document.getElementById('baseUrl').value = 'http://localhost:11434/v1';
|
354 |
document.getElementById('apiKey').value = 'ollama';
|
|
|
358 |
function setLMStudioDefaults() {
|
359 |
document.getElementById('baseUrl').value = 'http://localhost:1234/v1';
|
360 |
document.getElementById('apiKey').value = 'lmstudio';
|
361 |
+
document.getElementById('modelName').value = 'mistralai/devstral-small-2507';
|
362 |
}
|
363 |
|
|
|
364 |
function logToElement(message, type = 'info', element_id) {
|
365 |
const output = document.getElementById(element_id);
|
366 |
const timestamp = new Date().toLocaleTimeString();
|
|
|
394 |
}
|
395 |
|
396 |
async function initializePyodide() {
|
397 |
+
// Check config first
|
398 |
+
const configCheck = checkConfig();
|
399 |
+
if (!configCheck.configLoaded) {
|
400 |
+
logInit("Config not loaded, but proceeding anyway", 'warning');
|
401 |
+
}
|
402 |
+
|
403 |
// Disable init button during setup
|
404 |
document.getElementById('initBtn').disabled = true;
|
405 |
|
|
|
417 |
###### YOUR PYTHON DEPENDENCIES ARE INSTALLED HERE ######
|
418 |
|
419 |
import micropip
|
420 |
+
await micropip.install("typing-extensions>=4.12.2")
|
421 |
+
await micropip.install("openai==1.99.9")
|
422 |
+
await micropip.install("mcp==1.12.4")
|
423 |
+
await micropip.install("openai-agents==0.2.6")
|
424 |
await micropip.install("sqlite3==1.0.0")
|
|
|
425 |
await micropip.install("markdownify==1.1.0")
|
426 |
+
await micropip.install("tavily-python==0.7.10")
|
427 |
`);
|
428 |
logInit("openai-agents installed successfully", 'success');
|
429 |
|
|
|
475 |
return;
|
476 |
}
|
477 |
|
478 |
+
// Check if Tavily API key is available
|
479 |
+
const configCheck = checkConfig();
|
480 |
+
const tavilyApiKey = configCheck.tavilyAvailable ? window.APP_CONFIG.TAVILY_API_KEY : null;
|
481 |
+
|
482 |
// Disable button during execution
|
483 |
document.getElementById('runBtn').disabled = true;
|
484 |
showRunning("Running Python code");
|
|
|
486 |
try {
|
487 |
logAgent("🤖 Setting up agent and running...");
|
488 |
logAgent(`Server: ${baseUrl} | Model: ${modelName}`);
|
489 |
+
if (tavilyApiKey) {
|
490 |
+
logAgent("Tavily search enabled", 'success');
|
491 |
+
} else {
|
492 |
+
logAgent("Tavily search disabled (no API key)", 'warning');
|
493 |
+
}
|
494 |
logAgent(`Prompt: "${prompt}"`);
|
495 |
|
496 |
// Run the agent and print everything in Python
|
|
|
510 |
)
|
511 |
from markdownify import markdownify
|
512 |
from requests.exceptions import RequestException
|
513 |
+
from tavily.tavily import TavilyClient
|
514 |
|
515 |
def _truncate_content(content: str, max_length: int) -> str:
|
516 |
if len(content) <= max_length:
|
|
|
527 |
return word.count(char)
|
528 |
|
529 |
@function_tool
|
530 |
+
def visit_webpage(url: str, timeout: int = 30, max_length: int = None) -> str:
|
531 |
"""Visits a webpage at the given url and reads its content as a markdown string. Use this to browse webpages.
|
532 |
|
533 |
Args:
|
534 |
url: The url of the webpage to visit.
|
535 |
+
timeout: The timeout in seconds for the request.
|
536 |
+
max_length: The maximum number of characters of text that can be returned.
|
537 |
+
If not provided, the full webpage is returned.
|
538 |
|
539 |
"""
|
540 |
try:
|
541 |
+
response = requests.get(url, timeout=timeout)
|
542 |
response.raise_for_status()
|
543 |
|
544 |
+
markdown_content = markdownify(response.text).strip()
|
545 |
|
546 |
markdown_content = re.sub(r"\\n{2,}", "\\n", markdown_content)
|
547 |
|
548 |
+
if max_length:
|
549 |
+
return _truncate_content(markdown_content, max_length)
|
550 |
+
|
551 |
+
return str(markdown_content)
|
552 |
+
|
553 |
except RequestException as e:
|
554 |
return f"Error fetching the webpage: {e!s}"
|
555 |
except Exception as e:
|
556 |
return f"An unexpected error occurred: {e!s}"
|
557 |
|
558 |
+
|
559 |
+
@function_tool
|
560 |
+
def search_tavily(query: str, include_images: bool = False) -> str:
|
561 |
+
"""Perform a Tavily web search based on your query and return the top search results.
|
562 |
+
|
563 |
+
See https://blog.tavily.com/getting-started-with-the-tavily-search-api for more information.
|
564 |
+
|
565 |
+
Args:
|
566 |
+
query (str): The search query to perform.
|
567 |
+
include_images (bool): Whether to include images in the results.
|
568 |
+
|
569 |
+
Returns:
|
570 |
+
The top search results as a formatted string.
|
571 |
+
|
572 |
+
"""
|
573 |
+
api_key='${tavilyApiKey ? tavilyApiKey.replace(/'/g, "\\'") : ""}'
|
574 |
+
|
575 |
+
if not api_key:
|
576 |
+
return "TAVILY_API_KEY not configured in config.js."
|
577 |
+
try:
|
578 |
+
client = TavilyClient(api_key)
|
579 |
+
response = client.search(query, include_images=include_images)
|
580 |
+
results = response.get("results", [])
|
581 |
+
output = []
|
582 |
+
for result in results:
|
583 |
+
output.append(
|
584 |
+
f"[{result.get('title', 'No Title')}]({result.get('url', '#')})\\n{result.get('content', '')}"
|
585 |
+
)
|
586 |
+
if include_images and "images" in response:
|
587 |
+
output.append("\\nImages:")
|
588 |
+
for image in response["images"]:
|
589 |
+
output.append(image)
|
590 |
+
return "\\n\\n".join(output) if output else "No results found."
|
591 |
+
except Exception as e:
|
592 |
+
return f"Error performing Tavily search: {e!s}"
|
593 |
+
|
594 |
+
|
595 |
async def test_agent():
|
596 |
print("=== STARTING AGENT TEST ===")
|
597 |
try:
|
|
|
603 |
)
|
604 |
set_default_openai_client(external_client)
|
605 |
|
606 |
+
# Build tools list conditionally
|
607 |
+
tools = [count_character_occurrences, visit_webpage]
|
608 |
+
${tavilyApiKey ? 'tools.append(search_tavily)' : '# search_tavily tool not added (no API key)'}
|
609 |
+
|
610 |
agent = Agent(
|
611 |
name="Tool caller",
|
612 |
instructions="You are a helpful agent. Use the available tools to answer the questions.",
|
613 |
+
tools=tools,
|
614 |
model=OpenAIChatCompletionsModel(
|
615 |
model="${modelName.replace(/'/g, "\\'")}",
|
616 |
openai_client=external_client,
|