feat: live PhoneLLM (pipecat-ai/phonellm-alpha-1) endpoint integration with full tool calling protocol
Browse files- index.html +224 -1
index.html
CHANGED
|
@@ -117,6 +117,45 @@
|
|
| 117 |
position: relative;
|
| 118 |
}
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
/* TTS Controls Banner */
|
| 121 |
.tts-controls-box {
|
| 122 |
background: #fefce8;
|
|
@@ -428,6 +467,25 @@
|
|
| 428 |
<!-- Left Voice & Speech Bubble Panel -->
|
| 429 |
<div class="panel">
|
| 430 |
<!-- TTS Engine & Voice Config -->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
<div class="tts-controls-box">
|
| 432 |
<div class="tts-row">
|
| 433 |
<span>🧠 <strong>TTS Engine:</strong></span>
|
|
@@ -689,11 +747,176 @@
|
|
| 689 |
return { comic: await toolGetRandomXkcd(), topic: cleanTopic };
|
| 690 |
}
|
| 691 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 692 |
// PhoneLLM Conversational & Tool Calling Engine
|
| 693 |
-
|
| 694 |
statusBadge.textContent = "⚡ PhoneLLM Reasoning...";
|
| 695 |
addMessage(userText, "user");
|
| 696 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
const lower = userText.toLowerCase();
|
| 698 |
let comic = null;
|
| 699 |
let toolName = null;
|
|
|
|
| 117 |
position: relative;
|
| 118 |
}
|
| 119 |
|
| 120 |
+
|
| 121 |
+
.endpoint-details {
|
| 122 |
+
background: #fefce8;
|
| 123 |
+
border: 2px solid var(--ink);
|
| 124 |
+
border-radius: 12px;
|
| 125 |
+
padding: 10px 14px;
|
| 126 |
+
margin-bottom: 14px;
|
| 127 |
+
box-shadow: var(--shadow-hard-sm);
|
| 128 |
+
font-size: 0.9rem;
|
| 129 |
+
font-weight: 700;
|
| 130 |
+
}
|
| 131 |
+
.endpoint-details summary {
|
| 132 |
+
cursor: pointer;
|
| 133 |
+
outline: none;
|
| 134 |
+
user-select: none;
|
| 135 |
+
}
|
| 136 |
+
.endpoint-config-grid {
|
| 137 |
+
display: flex;
|
| 138 |
+
flex-direction: column;
|
| 139 |
+
gap: 8px;
|
| 140 |
+
margin-top: 10px;
|
| 141 |
+
padding-top: 8px;
|
| 142 |
+
border-top: 2px dashed var(--ink);
|
| 143 |
+
}
|
| 144 |
+
.input-group label {
|
| 145 |
+
display: block;
|
| 146 |
+
font-size: 0.8rem;
|
| 147 |
+
font-family: 'JetBrains Mono', monospace;
|
| 148 |
+
margin-bottom: 3px;
|
| 149 |
+
}
|
| 150 |
+
.input-group input {
|
| 151 |
+
width: 100%;
|
| 152 |
+
padding: 6px 10px;
|
| 153 |
+
border: 1.5px solid var(--ink);
|
| 154 |
+
border-radius: 6px;
|
| 155 |
+
font-size: 0.85rem;
|
| 156 |
+
font-family: 'JetBrains Mono', monospace;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
/* TTS Controls Banner */
|
| 160 |
.tts-controls-box {
|
| 161 |
background: #fefce8;
|
|
|
|
| 467 |
<!-- Left Voice & Speech Bubble Panel -->
|
| 468 |
<div class="panel">
|
| 469 |
<!-- TTS Engine & Voice Config -->
|
| 470 |
+
<!-- PhoneLLM Model Endpoint Configuration -->
|
| 471 |
+
<details class="endpoint-details">
|
| 472 |
+
<summary>⚙️ <strong>PhoneLLM Model Endpoint Settings (vLLM / Modal / SGLang / HF)</strong></summary>
|
| 473 |
+
<div class="endpoint-config-grid">
|
| 474 |
+
<div class="input-group">
|
| 475 |
+
<label>API Base URL (OpenAI-compatible):</label>
|
| 476 |
+
<input id="api-base-url" type="text" placeholder="e.g. https://your-modal-app.modal.run/v1 or http://localhost:8000/v1" />
|
| 477 |
+
</div>
|
| 478 |
+
<div class="input-group">
|
| 479 |
+
<label>API Key (Optional):</label>
|
| 480 |
+
<input id="api-key" type="password" placeholder="Bearer Token / API Key" />
|
| 481 |
+
</div>
|
| 482 |
+
<div class="input-group">
|
| 483 |
+
<label>Model ID:</label>
|
| 484 |
+
<input id="model-id" type="text" value="pipecat-ai/phonellm-alpha-1" />
|
| 485 |
+
</div>
|
| 486 |
+
</div>
|
| 487 |
+
</details>
|
| 488 |
+
|
| 489 |
<div class="tts-controls-box">
|
| 490 |
<div class="tts-row">
|
| 491 |
<span>🧠 <strong>TTS Engine:</strong></span>
|
|
|
|
| 747 |
return { comic: await toolGetRandomXkcd(), topic: cleanTopic };
|
| 748 |
}
|
| 749 |
|
| 750 |
+
|
| 751 |
+
const apiBaseInput = document.getElementById('api-base-url');
|
| 752 |
+
const apiKeyInput = document.getElementById('api-key');
|
| 753 |
+
const modelIdInput = document.getElementById('model-id');
|
| 754 |
+
|
| 755 |
+
// PhoneLLM Tool Definition Schema (OpenAI Compatible)
|
| 756 |
+
const PHONELLM_TOOLS_SCHEMA = [
|
| 757 |
+
{
|
| 758 |
+
type: "function",
|
| 759 |
+
function: {
|
| 760 |
+
name: "get_latest_xkcd",
|
| 761 |
+
description: "Fetch the latest published XKCD comic with its title, image URL, and alt-text punchline.",
|
| 762 |
+
parameters: { type: "object", properties: {}, required: [] }
|
| 763 |
+
}
|
| 764 |
+
},
|
| 765 |
+
{
|
| 766 |
+
type: "function",
|
| 767 |
+
function: {
|
| 768 |
+
name: "get_random_xkcd",
|
| 769 |
+
description: "Fetch a random XKCD comic strip from the entire archive.",
|
| 770 |
+
parameters: { type: "object", properties: {}, required: [] }
|
| 771 |
+
}
|
| 772 |
+
},
|
| 773 |
+
{
|
| 774 |
+
type: "function",
|
| 775 |
+
function: {
|
| 776 |
+
name: "search_xkcd_by_topic",
|
| 777 |
+
description: "Search or retrieve an XKCD comic about a specific topic (e.g. 'python', 'git', 'sql', 'ai', 'tar', 'regex', 'compiling').",
|
| 778 |
+
parameters: {
|
| 779 |
+
type: "object",
|
| 780 |
+
properties: {
|
| 781 |
+
topic: { type: "string", description: "The topic or programming concept to search for." }
|
| 782 |
+
},
|
| 783 |
+
required: ["topic"]
|
| 784 |
+
}
|
| 785 |
+
}
|
| 786 |
+
},
|
| 787 |
+
{
|
| 788 |
+
type: "function",
|
| 789 |
+
function: {
|
| 790 |
+
name: "get_xkcd_by_number",
|
| 791 |
+
description: "Fetch a specific XKCD comic by its exact comic number.",
|
| 792 |
+
parameters: {
|
| 793 |
+
type: "object",
|
| 794 |
+
properties: {
|
| 795 |
+
comic_num: { type: "integer", description: "The exact comic number to retrieve." }
|
| 796 |
+
},
|
| 797 |
+
required: ["comic_num"]
|
| 798 |
+
}
|
| 799 |
+
}
|
| 800 |
+
}
|
| 801 |
+
];
|
| 802 |
+
|
| 803 |
+
let conversationHistory = [
|
| 804 |
+
{
|
| 805 |
+
role: "system",
|
| 806 |
+
content: "You are XKCD Voice Bot, powered by pipecat-ai/phonellm-alpha-1. You have live tool access to https://xkcd.hemanth.deno.net. Always use your tools to look up comics. Keep responses punchy and mention the title, comic number, and punchline alt-text."
|
| 807 |
+
}
|
| 808 |
+
];
|
| 809 |
+
|
| 810 |
+
// Call live PhoneLLM vLLM / Modal endpoint if configured
|
| 811 |
+
async function callLivePhoneLLMEndpoint(userText) {
|
| 812 |
+
const baseUrl = (apiBaseInput.value || "").trim().replace(/\/+$/, "");
|
| 813 |
+
if (!baseUrl) return null;
|
| 814 |
+
|
| 815 |
+
const apiKey = (apiKeyInput.value || "").trim();
|
| 816 |
+
const model = (modelIdInput.value || "pipecat-ai/phonellm-alpha-1").trim();
|
| 817 |
+
|
| 818 |
+
conversationHistory.push({ role: "user", content: userText });
|
| 819 |
+
|
| 820 |
+
const headers = { "Content-Type": "application/json" };
|
| 821 |
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
| 822 |
+
|
| 823 |
+
try {
|
| 824 |
+
statusBadge.textContent = "⚡ Calling live PhoneLLM model...";
|
| 825 |
+
// Call PhoneLLM with temperature=0 & thinking disabled (per PhoneLLM spec)
|
| 826 |
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
| 827 |
+
method: "POST",
|
| 828 |
+
headers: headers,
|
| 829 |
+
body: JSON.stringify({
|
| 830 |
+
model: model,
|
| 831 |
+
messages: conversationHistory,
|
| 832 |
+
tools: PHONELLM_TOOLS_SCHEMA,
|
| 833 |
+
tool_choice: "auto",
|
| 834 |
+
temperature: 0.0
|
| 835 |
+
})
|
| 836 |
+
});
|
| 837 |
+
|
| 838 |
+
if (!response.ok) {
|
| 839 |
+
throw new Error(`PhoneLLM endpoint returned HTTP ${response.status}`);
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
const data = await response.json();
|
| 843 |
+
const choice = data.choices[0];
|
| 844 |
+
const message = choice.message;
|
| 845 |
+
let comic = null;
|
| 846 |
+
let toolName = null;
|
| 847 |
+
|
| 848 |
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
| 849 |
+
conversationHistory.push(message);
|
| 850 |
+
|
| 851 |
+
for (const tc of message.tool_calls) {
|
| 852 |
+
const fnName = tc.function.name;
|
| 853 |
+
const fnArgs = JSON.parse(tc.function.arguments || "{}");
|
| 854 |
+
toolName = `${fnName}(${JSON.stringify(fnArgs)})`;
|
| 855 |
+
statusBadge.textContent = `⚙️ PhoneLLM invoked tool: ${fnName}...`;
|
| 856 |
+
|
| 857 |
+
let toolResult = null;
|
| 858 |
+
if (fnName === "get_latest_xkcd") {
|
| 859 |
+
toolResult = await toolGetLatestXkcd();
|
| 860 |
+
comic = toolResult;
|
| 861 |
+
} else if (fnName === "get_random_xkcd") {
|
| 862 |
+
toolResult = await toolGetRandomXkcd();
|
| 863 |
+
comic = toolResult;
|
| 864 |
+
} else if (fnName === "get_xkcd_by_number") {
|
| 865 |
+
toolResult = await toolGetXkcdByNumber(fnArgs.comic_num || 1);
|
| 866 |
+
comic = toolResult;
|
| 867 |
+
} else if (fnName === "search_xkcd_by_topic") {
|
| 868 |
+
const res = await toolSearchXkcdByTopic(fnArgs.topic || "python");
|
| 869 |
+
toolResult = res.comic;
|
| 870 |
+
comic = toolResult;
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
conversationHistory.push({
|
| 874 |
+
role: "tool",
|
| 875 |
+
tool_call_id: tc.id,
|
| 876 |
+
content: JSON.stringify(toolResult)
|
| 877 |
+
});
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
// Request final voice reply from PhoneLLM
|
| 881 |
+
const finalRes = await fetch(`${baseUrl}/chat/completions`, {
|
| 882 |
+
method: "POST",
|
| 883 |
+
headers: headers,
|
| 884 |
+
body: JSON.stringify({
|
| 885 |
+
model: model,
|
| 886 |
+
messages: conversationHistory,
|
| 887 |
+
temperature: 0.0
|
| 888 |
+
})
|
| 889 |
+
});
|
| 890 |
+
|
| 891 |
+
const finalData = await finalRes.json();
|
| 892 |
+
const finalReply = finalData.choices[0].message.content;
|
| 893 |
+
conversationHistory.push({ role: "assistant", content: finalReply });
|
| 894 |
+
|
| 895 |
+
return { replyText: finalReply, comic: comic, toolName: toolName };
|
| 896 |
+
} else {
|
| 897 |
+
conversationHistory.push({ role: "assistant", content: message.content });
|
| 898 |
+
return { replyText: message.content, comic: null, toolName: null };
|
| 899 |
+
}
|
| 900 |
+
} catch (err) {
|
| 901 |
+
console.warn("Live PhoneLLM API failed, falling back to local engine:", err);
|
| 902 |
+
return null;
|
| 903 |
+
}
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
// PhoneLLM Conversational & Tool Calling Engine
|
| 907 |
+
async function processPhoneLLMTurn(userText) {
|
| 908 |
statusBadge.textContent = "⚡ PhoneLLM Reasoning...";
|
| 909 |
addMessage(userText, "user");
|
| 910 |
|
| 911 |
+
// Try Live PhoneLLM Endpoint first if configured by user
|
| 912 |
+
const liveResult = await callLivePhoneLLMEndpoint(userText);
|
| 913 |
+
if (liveResult) {
|
| 914 |
+
addMessage(liveResult.replyText, "agent", liveResult.toolName);
|
| 915 |
+
if (liveResult.comic) renderComic(liveResult.comic);
|
| 916 |
+
speak(liveResult.replyText);
|
| 917 |
+
return;
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
const lower = userText.toLowerCase();
|
| 921 |
let comic = null;
|
| 922 |
let toolName = null;
|