gnumanth commited on
Commit
ad9fd32
·
1 Parent(s): 9f694c9

feat: live PhoneLLM (pipecat-ai/phonellm-alpha-1) endpoint integration with full tool calling protocol

Browse files
Files changed (1) hide show
  1. 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
- async function processPhoneLLMTurn(userText) {
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;