abdibrokhim commited on
Commit
643c34b
·
1 Parent(s): 122ac3d

added tutorial

Browse files
Files changed (3) hide show
  1. TUTORIAL.md +918 -0
  2. app.py +0 -2
  3. prompts-followup.md +638 -1
TUTORIAL.md ADDED
@@ -0,0 +1,918 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Step-by-Step Tutorial: Building "Bagoodex Web Search"
2
+
3
+ This tutorial provides a structured walkthrough to create "Bagoodex Web Search," an open-source Perplexity-like app built with Python, Gradio, and external APIs. We'll be using the AI/ML API for AI capabilities.
4
+
5
+ ### AI/ML API
6
+ AI/ML API is a game-changing platform for developers and SaaS entrepreneurs looking to integrate cutting-edge AI capabilities into their products. It offers a single point of access to over 200 state-of-the-art AI models, covering everything from NLP to computer vision.
7
+
8
+ Key Features for Developers:
9
+ - Extensive Model Library: 200+ pre-trained models for rapid prototyping and deployment. 📚
10
+ - Customization Options: Fine-tune models to fit your specific use case. 🎯
11
+ - Developer-Friendly Integration: RESTful APIs and SDKs for seamless incorporation into your stack. 🛠️
12
+ - Serverless Architecture: Focus on coding, not infrastructure management. ☁️
13
+
14
+ [Get Started for FREE](https://aimlapi.com/?via=ibrohim).
15
+
16
+ [Deep Dive](https://docs.aimlapi.com/) into AI/ML API Documentation (very detailed, can’t agree more).
17
+
18
+ ---
19
+
20
+ ### Step 1: Setting Up the Environment
21
+
22
+ **1.1 Create a Virtual Environment:**
23
+
24
+ ```bash
25
+ python -m venv .venv
26
+ source .venv/bin/activate
27
+ ```
28
+
29
+ **1.2 Install Dependencies:** Create and populate `[requirements.txt]` with:
30
+
31
+ ```text
32
+ openai
33
+ gradio
34
+ python-dotenv
35
+ requests
36
+ pytube
37
+ ```
38
+
39
+ Then install them:
40
+
41
+ ```bash
42
+ pip install -r requirements.txt
43
+ ```
44
+
45
+ **1.3 Environment Variables:** Create a `.env` file with your API keys:
46
+
47
+ ```text
48
+ AIML_API_KEY=your_api_key
49
+ GOOGLE_MAPS_API_KEY=your_google_maps_api_key
50
+ ```
51
+
52
+ > Here's a brief tutorial: [How to get API Key from AI/ML API. Quick step-by-step tutorial with screenshots for better understanding](https://medium.com/@abdibrokhim/how-to-get-api-key-from-ai-ml-api-225a69d0bb25).
53
+
54
+ **1.4 Git Ignore:** Add `.gitignore`:
55
+
56
+ ```text
57
+ .env
58
+ .venv
59
+ __pycache__
60
+ *.pyc
61
+ .DS_Store
62
+ ```
63
+
64
+ ---
65
+
66
+ ### Step 2: Project Structure
67
+
68
+ Your final project directory should look like:
69
+
70
+ ```bash
71
+ Bagoodex_Web_Search/
72
+ ├── .env
73
+ ├── .gitignore
74
+ ├── requirements.txt
75
+ ├── app.py
76
+ ├── bagoodex_client.py
77
+ ├── helpers.py
78
+ ├── prompts.py
79
+ └── r_types.py
80
+ ```
81
+
82
+ ---
83
+
84
+ ### Step 3: Key Files Explained
85
+
86
+ #### 3.1 `[bagoodex_client.py]`
87
+
88
+ - Implements API interactions with `bagoodex` and GPT services.
89
+
90
+ - Import necessary modules:
91
+ ```py
92
+ import os
93
+ import requests
94
+ from openai import OpenAI
95
+ from dotenv import load_dotenv
96
+ from r_types import ChatMessage
97
+ from prompts import SYSTEM_PROMPT_BASE, SYSTEM_PROMPT_MAP
98
+ from typing import List
99
+ ```
100
+
101
+ - Load environment variables and set up the API client:
102
+
103
+
104
+ ```py
105
+ load_dotenv()
106
+ API_KEY = os.getenv("AIML_API_KEY")
107
+ API_URL = "https://api.aimlapi.com"
108
+ ```
109
+
110
+ - Define the `BagoodexClient` class:
111
+
112
+ ```py
113
+ class BagoodexClient:
114
+ def __init__(self, api_key=API_KEY, api_url=API_URL):
115
+ self.api_key = api_key
116
+ self.api_url = api_url
117
+ self.client = OpenAI(base_url=self.api_url, api_key=self.api_key)
118
+ ```
119
+
120
+ - Includes methods:
121
+ - `complete_chat()`: Handles general chat interactions.
122
+
123
+ ```py
124
+ def complete_chat(self, query):
125
+ """
126
+ Calls the standard chat completion endpoint using the provided query.
127
+ Returns the generated followup ID and the text response.
128
+ """
129
+ response = self.client.chat.completions.create(
130
+ model="bagoodex/bagoodex-search-v1",
131
+ messages=[
132
+ ChatMessage(role="user", content=SYSTEM_PROMPT_BASE),
133
+ ChatMessage(role="user", content=query)
134
+ ],
135
+ )
136
+ followup_id = response.id # the unique ID for follow-up searches
137
+ answer = response.choices[0].message.content
138
+ return followup_id, answer
139
+ ```
140
+ - `base_qna()`: Handles basic Q&A interactions. Basically we'll use this for follow-up questions. It's pretty reusable. We should pass the different system prompts based on our use case.
141
+ ```py
142
+ def base_qna(self, messages: List[ChatMessage], system_prompt=SYSTEM_PROMPT_BASE):
143
+ response = self.client.chat.completions.create(
144
+ model="gpt-4o",
145
+ messages=[
146
+ ChatMessage(role="user", content=system_prompt),
147
+ *messages
148
+ ],
149
+ )
150
+ return response.choices[0].message.content
151
+ ```
152
+
153
+ - Retrieves IDs for fetching follow-up resources (links, images, videos, maps).
154
+ ```py
155
+ def get_links(self, followup_id):
156
+ headers = {"Authorization": f"Bearer {self.api_key}"}
157
+ params = {"followup_id": followup_id}
158
+ response = requests.get(
159
+ f"{self.api_url}/v1/bagoodex/links", headers=headers, params=params
160
+ )
161
+ return response.json()
162
+
163
+ def get_images(self, followup_id):
164
+ headers = {"Authorization": f"Bearer {self.api_key}"}
165
+ params = {"followup_id": followup_id}
166
+ response = requests.get(
167
+ f"{self.api_url}/v1/bagoodex/images", headers=headers, params=params
168
+ )
169
+ return response.json()
170
+
171
+ def get_videos(self, followup_id):
172
+ headers = {"Authorization": f"Bearer {self.api_key}"}
173
+ params = {"followup_id": followup_id}
174
+ response = requests.get(
175
+ f"{self.api_url}/v1/bagoodex/videos", headers=headers, params=params
176
+ )
177
+ return response.json()
178
+
179
+ def get_local_map(self, followup_id):
180
+ headers = {"Authorization": f"Bearer {self.api_key}"}
181
+ params = {"followup_id": followup_id}
182
+ response = requests.get(
183
+ f"{self.api_url}/v1/bagoodex/local-map", headers=headers, params=params
184
+ )
185
+ return response.json()
186
+
187
+ def get_knowledge(self, followup_id):
188
+ headers = {"Authorization": f"Bearer {self.api_key}"}
189
+ params = {"followup_id": followup_id}
190
+ response = requests.get(
191
+ f"{self.api_url}/v1/bagoodex/knowledge", headers=headers, params=params
192
+ )
193
+ return response.json()
194
+ ```
195
+
196
+ Note: First, you must first call the standard chat completion endpoint `complete_chat()` with your query. The chat completion endpoint returns an ID, which must then be passed as the sole input parameter `followup_id` to the `bagoodex/links`, `bagoodex/images`, `bagoodex/videos`, `bagoodex/local-map` and `bagoodex/knowledge` endpoints.
197
+
198
+ #### 3.2 `[app.py]`
199
+
200
+ - Import necessary modules:
201
+ ```py
202
+ import os
203
+ import gradio as gr
204
+ from bagoodex_client import BagoodexClient
205
+ from r_types import ChatMessage
206
+ from prompts import (
207
+ SYSTEM_PROMPT_FOLLOWUP,
208
+ SYSTEM_PROMPT_MAP,
209
+ SYSTEM_PROMPT_BASE,
210
+ SYSTEM_PROMPT_KNOWLEDGE_BASE
211
+ )
212
+ from helpers import (
213
+ embed_video,
214
+ format_links,
215
+ embed_google_map,
216
+ format_knowledge,
217
+ format_followup_questions
218
+ )
219
+ ```
220
+
221
+ - Initialize the `BagoodexClient`:
222
+
223
+ ```py
224
+ client = BagoodexClient()
225
+ ```
226
+
227
+ - Central application logic.
228
+ ```py
229
+ # ----------------------------
230
+ # Chat & Follow-up Functions
231
+ # ----------------------------
232
+ def chat_function(message, history, followup_state, chat_history_state):
233
+ """
234
+ Process a new user message.
235
+ Appends the message and response to the conversation,
236
+ and retrieves follow-up questions.
237
+ """
238
+ # complete_chat returns a new followup id and answer
239
+ followup_id_new, answer = client.complete_chat(message)
240
+ # Update conversation history (if history is None, use an empty list)
241
+ if history is None:
242
+ history = []
243
+ updated_history = history + [ChatMessage({"role": "user", "content": message}),
244
+ ChatMessage({"role": "assistant", "content": answer})]
245
+ # Retrieve follow-up questions using the updated conversation
246
+ followup_questions_raw = client.base_qna(
247
+ messages=updated_history, system_prompt=SYSTEM_PROMPT_FOLLOWUP
248
+ )
249
+ # Format them using the helper
250
+ followup_md = format_followup_questions(followup_questions_raw)
251
+ return answer, followup_id_new, updated_history, followup_md
252
+
253
+ def handle_followup_click(question, followup_state, chat_history_state):
254
+ """
255
+ When a follow-up question is clicked, send it as a new message.
256
+ """
257
+ if not question:
258
+ return chat_history_state, followup_state, ""
259
+ # Process the follow-up question via complete_chat
260
+ followup_id_new, answer = client.complete_chat(question)
261
+ updated_history = chat_history_state + [ChatMessage({"role": "user", "content": question}),
262
+ ChatMessage({"role": "assistant", "content": answer})]
263
+ # Get new follow-up questions
264
+ followup_questions_raw = client.base_qna(
265
+ messages=updated_history, system_prompt=SYSTEM_PROMPT_FOLLOWUP
266
+ )
267
+ followup_md = format_followup_questions(followup_questions_raw)
268
+ return updated_history, followup_id_new, followup_md
269
+ ```
270
+
271
+ - Next setup `Local map` and `Knowledge base` functions.
272
+ ```py
273
+ def handle_local_map_click(followup_state, chat_history_state):
274
+ """
275
+ On local map click, try to get a local map.
276
+ If issues occur, fall back to using the SYSTEM_PROMPT_MAP.
277
+ """
278
+ if not followup_state:
279
+ return chat_history_state
280
+ try:
281
+ result = client.get_local_map(followup_state)
282
+
283
+ if result:
284
+ map_url = result.get('link', '')
285
+ # Use helper to produce an embedded map iframe
286
+ html = embed_google_map(map_url)
287
+
288
+ # Fall back: use the base_qna call with SYSTEM_PROMPT_MAP
289
+ result = client.base_qna(
290
+ messages=chat_history_state, system_prompt=SYSTEM_PROMPT_MAP
291
+ )
292
+ # Assume result contains a 'link' field
293
+ html = embed_google_map(result.get('link', ''))
294
+ new_message = ChatMessage({"role": "assistant", "content": html})
295
+ return chat_history_state + [new_message]
296
+ except Exception:
297
+ return chat_history_state
298
+
299
+ def handle_knowledge_click(followup_state, chat_history_state):
300
+ """
301
+ On knowledge base click, fetch and format knowledge content.
302
+ """
303
+ if not followup_state:
304
+ return chat_history_state
305
+
306
+ try:
307
+ print('trying to get knowledge')
308
+ result = client.get_knowledge(followup_state)
309
+ knowledge_md = format_knowledge(result)
310
+
311
+ if knowledge_md == 0000:
312
+ print('falling back to base_qna')
313
+ # Fall back: use the base_qna call with SYSTEM_PROMPT_KNOWLEDGE_BASE
314
+ result = client.base_qna(
315
+ messages=chat_history_state, system_prompt=SYSTEM_PROMPT_KNOWLEDGE_BASE
316
+ )
317
+ knowledge_md = format_knowledge(result)
318
+ new_message = ChatMessage({"role": "assistant", "content": knowledge_md})
319
+ return chat_history_state + [new_message]
320
+ except Exception:
321
+ return chat_history_state
322
+ ```
323
+
324
+ - Advanced search functions.
325
+ ```py
326
+ # ----------------------------
327
+ # Advanced Search Functions
328
+ # ----------------------------
329
+ def perform_image_search(followup_state):
330
+ if not followup_state:
331
+ return []
332
+ result = client.get_images(followup_state)
333
+ # For images we simply return a list of original URLs
334
+ return [item.get("original", "") for item in result]
335
+
336
+ def perform_video_search(followup_state):
337
+ if not followup_state:
338
+ return "<p>No followup ID available.</p>"
339
+ result = client.get_videos(followup_state)
340
+ # Use the helper to produce the embed iframes (supports multiple videos)
341
+ return embed_video(result)
342
+
343
+ def perform_links_search(followup_state):
344
+ if not followup_state:
345
+ return gr.Markdown("No followup ID available.")
346
+ result = client.get_links(followup_state)
347
+ return format_links(result)
348
+ ```
349
+
350
+ - Uses `Gradio` for UI. Settign up CSS.
351
+ ```py
352
+ # ----------------------------
353
+ # UI Build
354
+ # ----------------------------
355
+ css = """
356
+ #chatbot {
357
+ height: 100%;
358
+ }
359
+ h1, h2, h3, h4, h5, h6 {
360
+ text-align: center;
361
+ display: block;
362
+ }
363
+ """
364
+ ```
365
+
366
+ - Handles chat, follow-up interactions, and advanced search features (images, videos, links).
367
+
368
+ ```py
369
+ with gr.Blocks(css=css, fill_height=True) as demo:
370
+ gr.Markdown("""
371
+ ## like perplexity, but with less features.
372
+ #### built by [@abdibrokhim](https://yaps.gg).
373
+ """)
374
+
375
+ # State variables to hold followup ID and conversation history, plus follow-up questions text
376
+ followup_state = gr.State(None)
377
+ chat_history_state = gr.State([]) # holds conversation history as a list of messages
378
+ followup_md_state = gr.State("") # holds follow-up questions as Markdown text
379
+
380
+ with gr.Row():
381
+ with gr.Column(scale=3):
382
+ with gr.Row():
383
+ btn_local_map = gr.Button("Local Map Search (coming soon...)", variant="secondary", size="sm", interactive=False)
384
+ btn_knowledge = gr.Button("Knowledge Base (coming soon...)", variant="secondary", size="sm", interactive=False)
385
+ # The ChatInterface now uses additional outputs for both followup_state and conversation history,
386
+ # plus follow-up questions Markdown.
387
+ chat = gr.ChatInterface(
388
+ fn=chat_function,
389
+ type="messages",
390
+ additional_inputs=[followup_state, chat_history_state],
391
+ additional_outputs=[followup_state, chat_history_state, followup_md_state],
392
+ )
393
+ # Button callbacks to append local map and knowledge base results to chat
394
+ btn_local_map.click(
395
+ fn=handle_local_map_click,
396
+ inputs=[followup_state, chat_history_state],
397
+ outputs=chat.chatbot
398
+ )
399
+ btn_knowledge.click(
400
+ fn=handle_knowledge_click,
401
+ inputs=[followup_state, chat_history_state],
402
+ outputs=chat.chatbot
403
+ )
404
+
405
+ # Radio-based follow-up questions
406
+ followup_radio = gr.Radio(
407
+ choices=[],
408
+ label="Follow-up Questions (select one and click 'Send Follow-up')"
409
+ )
410
+ btn_send_followup = gr.Button("Send Follow-up")
411
+
412
+ # When the user clicks "Send Follow-up", the selected question is passed
413
+ # to handle_followup_click
414
+ btn_send_followup.click(
415
+ fn=handle_followup_click,
416
+ inputs=[followup_radio, followup_state, chat_history_state],
417
+ outputs=[chat.chatbot, followup_state, followup_md_state]
418
+ )
419
+
420
+ # Update the radio choices when followup_md_state changes
421
+ def update_followup_radio(md_text):
422
+ """
423
+ Parse Markdown lines to extract questions starting with '- '.
424
+ """
425
+ lines = md_text.splitlines()
426
+ questions = []
427
+ for line in lines:
428
+ if line.startswith("- "):
429
+ questions.append(line[2:])
430
+ return gr.update(choices=questions, value=None)
431
+
432
+ followup_md_state.change(
433
+ fn=update_followup_radio,
434
+ inputs=[followup_md_state],
435
+ outputs=[followup_radio]
436
+ )
437
+
438
+ with gr.Column(scale=1):
439
+ gr.Markdown("### Advanced Search Options")
440
+ with gr.Column(variant="panel"):
441
+ btn_images = gr.Button("Search Images")
442
+ btn_videos = gr.Button("Search Videos")
443
+ btn_links = gr.Button("Search Links")
444
+ gallery_output = gr.Gallery(label="Image Results", columns=2)
445
+ video_output = gr.HTML(label="Video Results") # HTML for embedded video iframes
446
+ links_output = gr.Markdown(label="Links Results")
447
+ btn_images.click(
448
+ fn=perform_image_search,
449
+ inputs=[followup_state],
450
+ outputs=[gallery_output]
451
+ )
452
+ btn_videos.click(
453
+ fn=perform_video_search,
454
+ inputs=[followup_state],
455
+ outputs=[video_output]
456
+ )
457
+ btn_links.click(
458
+ fn=perform_links_search,
459
+ inputs=[followup_state],
460
+ outputs=[links_output]
461
+ )
462
+ demo.launch()
463
+ ```
464
+
465
+ Questions you may consider to ask:
466
+
467
+ ```text
468
+ how to make slingshot?
469
+ who created light (e.g., electricity) Tesla or Edison in quick short?
470
+ ```
471
+
472
+ #### 3.2 `[helpers.py]`
473
+
474
+ - Utility functions for formatting results:
475
+ - import the necessary modules:
476
+ ```py
477
+ from dotenv import load_dotenv
478
+ import os
479
+ import gradio as gr
480
+ import urllib.parse
481
+ import re
482
+ from pytube import YouTube
483
+ from typing import List, Optional, Dict
484
+ from r_types import (
485
+ SearchVideosResponse,
486
+ SearchImagesResponse,
487
+ SearchLinksResponse,
488
+ LocalMapResponse,
489
+ KnowledgeBaseResponse
490
+ )
491
+ import json
492
+ ```
493
+ - `embed_video()` for YouTube videos
494
+ ```py
495
+ def get_video_id(url: str) -> Optional[str]:
496
+ """
497
+ Safely retrieve the YouTube video_id from a given URL using pytube.
498
+ Returns None if the URL is invalid or an error occurs.
499
+ """
500
+ if not url:
501
+ return None
502
+
503
+ try:
504
+ yt = YouTube(url)
505
+ return yt.video_id
506
+ except Exception:
507
+ # If the URL is invalid or pytube fails, return None
508
+ return None
509
+
510
+ def embed_video(videos: List[SearchVideosResponse]) -> str:
511
+ """
512
+ Given a list of video data (with 'link' and 'title'),
513
+ returns an HTML string of embedded YouTube iframes.
514
+ """
515
+ if not videos:
516
+ return "<p>No videos found.</p>"
517
+
518
+ # Collect each iframe snippet
519
+ iframes = []
520
+ for video in videos:
521
+ url = video.get("link", "")
522
+ video_id = get_video_id(url)
523
+ if not video_id:
524
+ # Skip invalid or non-parsable links
525
+ continue
526
+
527
+ title = video.get("title", "").replace('"', '\\"') # Escape quotes
528
+ iframe = f"""
529
+ <iframe
530
+ width="560"
531
+ height="315"
532
+ src="https://www.youtube.com/embed/{video_id}"
533
+ title="{title}"
534
+ frameborder="0"
535
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
536
+ allowfullscreen>
537
+ </iframe>
538
+ """
539
+ iframes.append(iframe)
540
+
541
+ # If no valid videos after processing, return a fallback message
542
+ if not iframes:
543
+ return "<p>No valid YouTube videos found.</p>"
544
+
545
+ # Join all iframes into one HTML string
546
+ return "\n".join(iframes)
547
+ ```
548
+ - `format_links()` for links
549
+ ```py
550
+ def format_links(links) -> str:
551
+ """
552
+ Convert a list of {'title': str, 'link': str} objects
553
+ into a bulleted Markdown string with clickable links.
554
+ """
555
+ if not links:
556
+ return "No links found."
557
+
558
+ links_md = "**Links:**\n"
559
+ for url in links:
560
+ title = url.rstrip('/').split('/')[-1]
561
+ links_md += f"- [{title}]({url})\n"
562
+ return links_md
563
+ ```
564
+ - `embed_google_map()` for maps
565
+ ```py
566
+ def embed_google_map(map_url: str) -> str:
567
+ """
568
+ Extracts a textual location from the given Google Maps URL
569
+ and returns an embedded Google Map iframe for that location.
570
+ Assumes you have a valid API key in place of 'YOUR_API_KEY'.
571
+ """
572
+ load_dotenv()
573
+ GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY")
574
+
575
+ if not map_url:
576
+ return "<p>Invalid Google Maps URL.</p>"
577
+
578
+ # Attempt to extract "San+Francisco,+CA" from the URL
579
+ match = re.search(r"/maps/place/([^/]+)", map_url)
580
+ if not match:
581
+ return "Invalid Google Maps URL. Could not extract location."
582
+
583
+ location_text = match.group(1)
584
+ # Remove query params or additional slashes from the captured group
585
+ location_text = re.split(r"[/?]", location_text)[0]
586
+
587
+ # URL-encode location to avoid issues with special characters
588
+ encoded_location = urllib.parse.quote(location_text, safe="")
589
+
590
+ embed_html = f"""
591
+ <iframe
592
+ width="600"
593
+ height="450"
594
+ style="border:0"
595
+ loading="lazy"
596
+ allowfullscreen
597
+ src="https://www.google.com/maps/embed/v1/place?key={GOOGLE_MAPS_API_KEY}&q={encoded_location}">
598
+ </iframe>
599
+ """
600
+ return embed_html
601
+ ```
602
+ - `format_knowledge()` for knowledge base info
603
+ ```py
604
+ def format_knowledge(raw_result: str) -> str:
605
+ """
606
+ Given a dictionary of knowledge data (e.g., about a person),
607
+ produce a Markdown string summarizing that info.
608
+ """
609
+
610
+ if not raw_result:
611
+ return 0000
612
+
613
+ # Clean up the raw JSON string
614
+ clean_json_str = cleanup_raw_json(raw_result)
615
+ print('Knowledge Data: ', clean_json_str)
616
+
617
+ try:
618
+ # Parse the cleaned JSON string
619
+ result = json.loads(clean_json_str)
620
+ title = result.get("title", "...")
621
+ type_ = result.get("type", "...")
622
+ born = result.get("born", "...")
623
+ died = result.get("died", "...")
624
+
625
+ content = f"""
626
+ **{title}**
627
+ Type: {type_}
628
+ Born: {born}
629
+ Died: {died}
630
+ """
631
+ return content
632
+ except json.JSONDecodeError:
633
+ return "Error: Failed to parse knowledge data."
634
+ ```
635
+ - Set up `format_followup_questions()` function that formats follow-up questions
636
+ ```py
637
+ def format_followup_questions(raw_questions: str) -> str:
638
+ """
639
+ Extracts and formats follow-up questions from a raw JSON-like string.
640
+
641
+ The input string may contain triple backticks (```json ... ```) which need to be removed before parsing.
642
+
643
+ Expected input format:
644
+ \```json
645
+ {
646
+ "followup_question": [
647
+ "What materials are needed to make a slingshot?",
648
+ "How to make a slingshot more powerful?"
649
+ ]
650
+ }
651
+ \```
652
+
653
+ Returns a Markdown-formatted string with the follow-up questions.
654
+ """
655
+
656
+ if not raw_questions:
657
+ return "No follow-up questions available."
658
+
659
+ # Clean up the raw JSON string
660
+ clean_json_str = cleanup_raw_json(raw_questions)
661
+
662
+ try:
663
+ # Parse the cleaned JSON string
664
+ questions_dict = json.loads(clean_json_str)
665
+
666
+ # Ensure the expected key exists
667
+ followup_list = questions_dict.get("followup_question", [])
668
+
669
+ if not isinstance(followup_list, list) or not followup_list:
670
+ return "No follow-up questions available."
671
+
672
+ # Format the questions into Markdown
673
+ questions_md = "### Follow-up Questions\n\n"
674
+ for question in followup_list:
675
+ questions_md += f"- {question}\n"
676
+
677
+ return questions_md
678
+
679
+ except json.JSONDecodeError:
680
+ return "Error: Failed to parse follow-up questions."
681
+ ```
682
+ - `cleanup_row_json()` function to clean up raw JSON strings
683
+ ```py
684
+ def cleanup_raw_json(raw_json: str) -> str:
685
+ """
686
+ Remove triple backticks and 'json' from the beginning and end of a raw JSON string.
687
+ """
688
+ return re.sub(r"```json|```", "", raw_json).strip()
689
+ ```
690
+
691
+ #### 3.3 `[prompts.py]`
692
+
693
+ - Contains system prompts for various tasks:
694
+ For example:
695
+
696
+ `SYSTEM_PROMPT_BASE` - For general chat interactions.
697
+
698
+ ```py
699
+ SYSTEM_PROMPT_BASE = """
700
+ ######SYSTEM INIATED######
701
+ You will be given a conversation chat (e.g., text/ paragraph).
702
+ Answer the given conversation chat with a relevant response.
703
+
704
+ ######NOTE######
705
+ Be nice and polite in your responses!
706
+ ######SYSTEM SHUTDOWN######
707
+ """
708
+ ```
709
+
710
+ `SYSTEM_PROMPT_MAP` - For providing places based on the given content.
711
+
712
+ ```py
713
+ SYSTEM_PROMPT_MAP = """
714
+ ######SYSTEM INIATED######
715
+ You will be given a content from conversation chat (e.g., text/ paragraph).
716
+ Your task is to analyze the given content and provide different types of places as close as possible to the given content.
717
+ For exampl: If the given content (conversation chat) was about "How to make a slingshot", you can provide places like "Hardware store", "Woodworking shop", "Outdoor sports store", etc.
718
+ Make sure the places you provide are relevant to the given content. And as much as close to the given content, the better.
719
+ Your final output should be a list of places.
720
+ Here's JSON format example:
721
+ \```json
722
+ {
723
+ "places": ["Hardware store", "Woodworking shop", "Outdoor sports store"]
724
+ }
725
+ \```
726
+
727
+ ######NOTE######
728
+ Make sure to return only JSON data! Nothing else!
729
+ ######SYSTEM SHUTDOWN######
730
+ """
731
+
732
+ ```
733
+
734
+ `SYSTEM_PROMPT_FOLLOWUP` - For generating follow-up questions based on the given content.
735
+
736
+ ```py
737
+ SYSTEM_PROMPT_FOLLOWUP = """
738
+ ######SYSTEM INIATED######
739
+ You will be given a content from conversation chat (e.g., text/ paragraph).
740
+ Your task is to analyze the given content and provide a follow-up question based on the given content.
741
+ For example: If the given content (conversation chat) was about "How to make a slingshot", you can provide a follow-up question like "What materials are needed to make a slingshot?".
742
+ Make sure the follow-up question you provide is relevant to the given content.
743
+ Your final output should be a List of follow-up question.
744
+ Here's JSON format example:
745
+ \```json
746
+ {
747
+ "followup_question": ["What materials are needed to make a slingshot?", "How to make a slingshot more powerful?"]
748
+ }
749
+ \```
750
+ ######NOTE######
751
+ Make sure to return only JSON data! Nothing else!
752
+ ######SYSTEM SHUTDOWN######
753
+ """
754
+ ```
755
+
756
+ `SYSTEM_PROMPT_KNOWLEDGE_BASE` - For generating knowledge base responses based on the given content.
757
+
758
+ ```py
759
+ SYSTEM_PROMPT_KNOWLEDGE_BASE = """
760
+ ######SYSTEM INIATED######
761
+ You will be given a content from conversation chat (e.g., text/ paragraph).
762
+ Your task is to analyze the given content and provide a knowledge base response based on the given content.
763
+ For example: If the given content (conversation chat) was about "How to make a slingshot".
764
+ You should analyze it and find the exact creator or founder or inventor of the slingshot.
765
+ Let's assume you just found out that the slingshot was invented by "Charles Goodyear".
766
+ Then return `question` in a JSON format. (e.g., {"question": "Who is Charles Goodyear?"}).
767
+ Your final output should be a JSON data with the knowledge base response.
768
+ Here's JSON format example:
769
+ \```json
770
+ {
771
+ "question": "Who is Charles Goodyear?",
772
+ }
773
+ \```
774
+ ######NOTE######
775
+ Make sure to return only JSON data! Nothing else!
776
+ ######SYSTEM SHUTDOWN######
777
+ """
778
+ ```
779
+
780
+
781
+ #### 3.3 `[r_types.py]`
782
+
783
+ - Placeholder for custom types and schemas.
784
+ ```py
785
+ from typing import TypedDict
786
+
787
+ # [ChatMessage]:
788
+ # <response>
789
+ # {
790
+ # "role": "system",
791
+ # "content": "Hello, how can I help you today?"
792
+ # }
793
+ # </response>
794
+
795
+ class ChatMessage(TypedDict):
796
+ role: str
797
+ content: str
798
+
799
+
800
+ # [Search Videos]:
801
+ # <response>
802
+ # Videos:
803
+ # [{'link': 'https://www.youtube.com/watch?v=X9oWGuKypuY', 'thumbnail': 'https://dmwtgq8yidg0m.cloudfront.net/medium/d3G6HeC5BO93-video-thumb.jpeg', 'title': 'Easy Home Made Slingshot'}, {'link': 'https://www.youtube.com/watch?v=V2iZF8oAXHo&pp=ygUMI2d1bGVsaGFuZGxl', 'thumbnail': 'https://dmwtgq8yidg0m.cloudfront.net/medium/sb2Iw9Ug-Pne-video-thumb.jpeg', 'title': 'Making an Apple Wood Slingshot | Woodcraft'}]
804
+ # </response>
805
+
806
+ class SearchVideosResponse(TypedDict):
807
+ link: str
808
+ thumbnail: str
809
+ title: str
810
+
811
+
812
+ # [Search Images]:
813
+ # <response>
814
+ # [{'source': '', 'original': 'https://i.ytimg.com/vi/iYlJirFtYaA/sddefault.jpg', 'title': 'How to make a Slingshot using Pencils ...', 'source_name': 'YouTube'}, {'source': '', 'original': 'https://i.ytimg.com/vi/HWSkVaptzRA/maxresdefault.jpg', 'title': 'How to make a Slingshot at Home - YouTube', 'source_name': 'YouTube'}, {'source': '', 'original': 'https://content.instructables.com/FHB/VGF8/FHXUOJKJ/FHBVGF8FHXUOJKJ.jpg?auto=webp', 'title': 'Country Boy" Style Slingshot ...', 'source_name': 'Instructables'}, {'source': '', 'original': 'https://i.ytimg.com/vi/6wXqlJVw03U/maxresdefault.jpg', 'title': 'Make slingshot using popsicle stick ...', 'source_name': 'YouTube'}, {'source': '', 'original': 'https://ds-tc.prod.pbskids.org/designsquad/diy/DESIGN-SQUAD-42.jpg', 'title': 'Build | Indoor Slingshot . DESIGN SQUAD ...', 'source_name': 'PBS KIDS'}, {'source': '', 'original': 'https://i.ytimg.com/vi/wCxFkPLuNyA/maxresdefault.jpg', 'title': 'Paper Ninja Weapons ...', 'source_name': 'YouTube'}, {'source': '', 'original': 'https://i0.wp.com/makezine.com/wp-content/uploads/2015/01/slingshot1.jpg?fit=800%2C600&ssl=1', 'title': 'Rotating Bearings ...', 'source_name': 'Make Magazine'}, {'source': '', 'original': 'https://makeandtakes.com/wp-content/uploads/IMG_1144-1.jpg', 'title': 'Make a DIY Stick Slingshot Kids Craft', 'source_name': 'Make and Takes'}, {'source': '', 'original': 'https://i.ytimg.com/vi/X9oWGuKypuY/maxresdefault.jpg', 'title': 'Easy Home Made Slingshot - YouTube', 'source_name': 'YouTube'}, {'source': '', 'original': 'https://www.wikihow.com/images/thumb/4/41/Make-a-Sling-Shot-Step-7-Version-5.jpg/550px-nowatermark-Make-a-Sling-Shot-Step-7-Version-5.jpg', 'title': 'How to Make a Sling Shot: 15 Steps ...', 'source_name': 'wikiHow'}]
815
+ # </response>
816
+
817
+ class SearchImagesResponse(TypedDict):
818
+ source: str
819
+ original: str
820
+ title: str
821
+ source: str
822
+ source_name: str
823
+
824
+
825
+ # [Links]:
826
+ # <response>
827
+ # ['https://www.reddit.com/r/slingshots/comments/1d50p3e/how_to_build_a_sling_at_home_thats_not_shit/', 'https://www.instructables.com/Make-a-Giant-Slingshot/', 'https://www.mudandbloom.com/blog/stick-slingshot', 'https://pbskids.org/designsquad/build/indoor-slingshot/', 'https://www.instructables.com/How-to-Make-a-Slingshot-2/']
828
+ # </response>
829
+
830
+ class SearchLinksResponse(TypedDict):
831
+ title: str
832
+ link: str
833
+
834
+
835
+ ### Local Map Response:
836
+ # <response>
837
+ # {
838
+ # "link": "https://www.google.com/maps/place/San+Francisco,+CA/data=!4m2!3m1!1s0x80859a6d00690021:0x4a501367f076adff?sa=X&ved=2ahUKEwjqg7eNz9KLAxVCFFkFHWSPEeIQ8gF6BAgqEAA&hl=en",
839
+ # "image": "https://dmwtgq8yidg0m.cloudfront.net/images/TdNFUpcEvvHL-local-map.webp"
840
+ # }
841
+ # </response>
842
+
843
+ class LocalMapResponse(TypedDict):
844
+ link: str
845
+ imgae: str
846
+
847
+
848
+ ### Model Response:
849
+
850
+ # <response>
851
+ # ```
852
+ # {
853
+ # 'title': 'Nikola Tesla',
854
+ # 'type': 'Engineer and futurist',
855
+ # 'description': None,
856
+ # 'born': 'July 10, 1856, Smiljan, Croatia',
857
+ # 'died': 'January 7, 1943 (age 86 years), The New Yorker A Wyndham Hotel, New York, NY'
858
+ # }
859
+ # ```
860
+ # </response>
861
+
862
+ class KnowledgeBaseResponse(TypedDict):
863
+ title: str
864
+ type: str
865
+ description: str
866
+ born: str
867
+ died: str
868
+
869
+ ```
870
+
871
+ ---
872
+
873
+ ### Step 4: Running the Application
874
+
875
+ **4.1 Run **``**:**
876
+
877
+ ```bash
878
+ python3 app.py
879
+ ```
880
+
881
+ **4.2 Access the Application:**
882
+
883
+ - Open your browser and visit the provided Gradio URL (`http://127.0.0.1:7860`).
884
+
885
+ ---
886
+
887
+ ### Step 5: Application Features
888
+
889
+ #### **Basic Interaction:**
890
+
891
+ - Type queries directly into the chat interface.
892
+ - Receive AI-generated answers and relevant follow-up suggestions.
893
+
894
+ #### **Advanced Features:**
895
+
896
+ - Image, video, and link searches from the follow-up context.
897
+ - Knowledge base retrieval.
898
+ - Local map searches.
899
+
900
+ ---
901
+
902
+ ### Step 5: Customizing Your App
903
+
904
+ - Modify prompts in `[prompts.py]` to personalize AI behavior.
905
+ - Expand functionality by adding more helpers or API endpoints in `[bagoodex_client.py]`.
906
+ - Adjust UI and functionalities in `[app.py]`.
907
+
908
+ ---
909
+
910
+ ### Step 5: Deploying Your App
911
+
912
+ - Consider deploying on `Hugging Face Spaces`.
913
+
914
+ ---
915
+
916
+ ### Conclusion
917
+
918
+ In this tutorial, you built "Bagoodex Web Search," a versatile AI-powered search tool. You learned to interact with external APIs, handle follow-up interactions, and create a user-friendly interface with Gradio. You can now expand this project with more features and deploy it to share with others.
app.py CHANGED
@@ -145,8 +145,6 @@ h1, h2, h3, h4, h5, h6 {
145
  }
146
  """
147
 
148
- # like chatgpt, but with less features. built by @theo and @r_marked
149
-
150
  # defautl query: how to make slingshot?
151
  # who created light (e.g., electricity) Tesla or Edison in quick short?
152
  with gr.Blocks(css=css, fill_height=True) as demo:
 
145
  }
146
  """
147
 
 
 
148
  # defautl query: how to make slingshot?
149
  # who created light (e.g., electricity) Tesla or Edison in quick short?
150
  with gr.Blocks(css=css, fill_height=True) as demo:
prompts-followup.md CHANGED
@@ -1450,4 +1450,641 @@ No need to display two times follow up questions. Remove second one. Radio butto
1450
 
1451
 
1452
 
1453
- [follow_up]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1450
 
1451
 
1452
 
1453
+ [follow_up]:
1454
+
1455
+
1456
+ ----
1457
+
1458
+ [Tutorial]:
1459
+
1460
+
1461
+
1462
+
1463
+
1464
+
1465
+
1466
+
1467
+
1468
+ Okey. I just build an app. It's "Bagoodex Web Search" an open source implementation of Perplexity like app.
1469
+ Next thing: I will provide with the all files and implementations and informations that i have used while building the app. You need to write step-by-step tutorial. All the file names exactly matches text inside square brackets. For example: [app.py].
1470
+
1471
+
1472
+ [bagoodex_client.py]
1473
+ <|START|>
1474
+ ```
1475
+ import os
1476
+ import requests
1477
+ from openai import OpenAI
1478
+ from dotenv import load_dotenv
1479
+ from r_types import ChatMessage
1480
+ from prompts import SYSTEM_PROMPT_BASE, SYSTEM_PROMPT_MAP
1481
+ from typing import List
1482
+
1483
+ load_dotenv()
1484
+ API_KEY = os.getenv("AIML_API_KEY")
1485
+ API_URL = "https://api.aimlapi.com"
1486
+
1487
+ class BagoodexClient:
1488
+ def __init__(self, api_key=API_KEY, api_url=API_URL):
1489
+ self.api_key = api_key
1490
+ self.api_url = api_url
1491
+ self.client = OpenAI(base_url=self.api_url, api_key=self.api_key)
1492
+
1493
+ def complete_chat(self, query):
1494
+ """
1495
+ Calls the standard chat completion endpoint using the provided query.
1496
+ Returns the generated followup ID and the text response.
1497
+ """
1498
+ response = self.client.chat.completions.create(
1499
+ model="bagoodex/bagoodex-search-v1",
1500
+ messages=[
1501
+ ChatMessage(role="user", content=SYSTEM_PROMPT_BASE),
1502
+ ChatMessage(role="user", content=query)
1503
+ ],
1504
+ )
1505
+ followup_id = response.id # the unique ID for follow-up searches
1506
+ answer = response.choices[0].message.content
1507
+ return followup_id, answer
1508
+
1509
+ def base_qna(self, messages: List[ChatMessage], system_prompt=SYSTEM_PROMPT_BASE):
1510
+ response = self.client.chat.completions.create(
1511
+ model="gpt-4o",
1512
+ messages=[
1513
+ ChatMessage(role="user", content=system_prompt),
1514
+ *messages
1515
+ ],
1516
+ )
1517
+ return response.choices[0].message.content
1518
+
1519
+ def get_links(self, followup_id):
1520
+ headers = {"Authorization": f"Bearer {self.api_key}"}
1521
+ params = {"followup_id": followup_id}
1522
+ response = requests.get(
1523
+ f"{self.api_url}/v1/bagoodex/links", headers=headers, params=params
1524
+ )
1525
+ return response.json()
1526
+
1527
+ def get_images(self, followup_id):
1528
+ headers = {"Authorization": f"Bearer {self.api_key}"}
1529
+ params = {"followup_id": followup_id}
1530
+ response = requests.get(
1531
+ f"{self.api_url}/v1/bagoodex/images", headers=headers, params=params
1532
+ )
1533
+ return response.json()
1534
+
1535
+ def get_videos(self, followup_id):
1536
+ headers = {"Authorization": f"Bearer {self.api_key}"}
1537
+ params = {"followup_id": followup_id}
1538
+ response = requests.get(
1539
+ f"{self.api_url}/v1/bagoodex/videos", headers=headers, params=params
1540
+ )
1541
+ return response.json()
1542
+
1543
+ def get_local_map(self, followup_id):
1544
+ headers = {"Authorization": f"Bearer {self.api_key}"}
1545
+ params = {"followup_id": followup_id}
1546
+ response = requests.get(
1547
+ f"{self.api_url}/v1/bagoodex/local-map", headers=headers, params=params
1548
+ )
1549
+ return response.json()
1550
+
1551
+ def get_knowledge(self, followup_id):
1552
+ headers = {"Authorization": f"Bearer {self.api_key}"}
1553
+ params = {"followup_id": followup_id}
1554
+ response = requests.get(
1555
+ f"{self.api_url}/v1/bagoodex/knowledge", headers=headers, params=params
1556
+ )
1557
+ return response.json()
1558
+
1559
+ ```
1560
+ <|END|>
1561
+
1562
+
1563
+ ----
1564
+
1565
+ [app.py]
1566
+ <|START|>
1567
+ ```
1568
+ import os
1569
+ import gradio as gr
1570
+ from bagoodex_client import BagoodexClient
1571
+ from r_types import ChatMessage
1572
+ from prompts import (
1573
+ SYSTEM_PROMPT_FOLLOWUP,
1574
+ SYSTEM_PROMPT_MAP,
1575
+ SYSTEM_PROMPT_BASE,
1576
+ SYSTEM_PROMPT_KNOWLEDGE_BASE
1577
+ )
1578
+ from helpers import (
1579
+ embed_video,
1580
+ format_links,
1581
+ embed_google_map,
1582
+ format_knowledge,
1583
+ format_followup_questions
1584
+ )
1585
+
1586
+ client = BagoodexClient()
1587
+
1588
+ # ----------------------------
1589
+ # Chat & Follow-up Functions
1590
+ # ----------------------------
1591
+ def chat_function(message, history, followup_state, chat_history_state):
1592
+ """
1593
+ Process a new user message.
1594
+ Appends the message and response to the conversation,
1595
+ and retrieves follow-up questions.
1596
+ """
1597
+ # complete_chat returns a new followup id and answer
1598
+ followup_id_new, answer = client.complete_chat(message)
1599
+ # Update conversation history (if history is None, use an empty list)
1600
+ if history is None:
1601
+ history = []
1602
+ updated_history = history + [ChatMessage({"role": "user", "content": message}),
1603
+ ChatMessage({"role": "assistant", "content": answer})]
1604
+ # Retrieve follow-up questions using the updated conversation
1605
+ followup_questions_raw = client.base_qna(
1606
+ messages=updated_history, system_prompt=SYSTEM_PROMPT_FOLLOWUP
1607
+ )
1608
+ # Format them using the helper
1609
+ followup_md = format_followup_questions(followup_questions_raw)
1610
+ return answer, followup_id_new, updated_history, followup_md
1611
+
1612
+ def handle_followup_click(question, followup_state, chat_history_state):
1613
+ """
1614
+ When a follow-up question is clicked, send it as a new message.
1615
+ """
1616
+ if not question:
1617
+ return chat_history_state, followup_state, ""
1618
+ # Process the follow-up question via complete_chat
1619
+ followup_id_new, answer = client.complete_chat(question)
1620
+ updated_history = chat_history_state + [ChatMessage({"role": "user", "content": question}),
1621
+ ChatMessage({"role": "assistant", "content": answer})]
1622
+ # Get new follow-up questions
1623
+ followup_questions_raw = client.base_qna(
1624
+ messages=updated_history, system_prompt=SYSTEM_PROMPT_FOLLOWUP
1625
+ )
1626
+ followup_md = format_followup_questions(followup_questions_raw)
1627
+ return updated_history, followup_id_new, followup_md
1628
+
1629
+ def handle_local_map_click(followup_state, chat_history_state):
1630
+ """
1631
+ On local map click, try to get a local map.
1632
+ If issues occur, fall back to using the SYSTEM_PROMPT_MAP.
1633
+ """
1634
+ if not followup_state:
1635
+ return chat_history_state
1636
+ try:
1637
+ result = client.get_local_map(followup_state)
1638
+
1639
+ if result:
1640
+ map_url = result.get('link', '')
1641
+ # Use helper to produce an embedded map iframe
1642
+ html = embed_google_map(map_url)
1643
+
1644
+ # Fall back: use the base_qna call with SYSTEM_PROMPT_MAP
1645
+ result = client.base_qna(
1646
+ messages=chat_history_state, system_prompt=SYSTEM_PROMPT_MAP
1647
+ )
1648
+ # Assume result contains a 'link' field
1649
+ html = embed_google_map(result.get('link', ''))
1650
+ new_message = ChatMessage({"role": "assistant", "content": html})
1651
+ return chat_history_state + [new_message]
1652
+ except Exception:
1653
+ return chat_history_state
1654
+
1655
+ def handle_knowledge_click(followup_state, chat_history_state):
1656
+ """
1657
+ On knowledge base click, fetch and format knowledge content.
1658
+ """
1659
+ if not followup_state:
1660
+ return chat_history_state
1661
+
1662
+ try:
1663
+ print('trying to get knowledge')
1664
+ result = client.get_knowledge(followup_state)
1665
+ knowledge_md = format_knowledge(result)
1666
+
1667
+ if knowledge_md == 0000:
1668
+ print('falling back to base_qna')
1669
+ # Fall back: use the base_qna call with SYSTEM_PROMPT_KNOWLEDGE_BASE
1670
+ result = client.base_qna(
1671
+ messages=chat_history_state, system_prompt=SYSTEM_PROMPT_KNOWLEDGE_BASE
1672
+ )
1673
+ knowledge_md = format_knowledge(result)
1674
+ new_message = ChatMessage({"role": "assistant", "content": knowledge_md})
1675
+ return chat_history_state + [new_message]
1676
+ except Exception:
1677
+ return chat_history_state
1678
+
1679
+ # ----------------------------
1680
+ # Advanced Search Functions
1681
+ # ----------------------------
1682
+ def perform_image_search(followup_state):
1683
+ if not followup_state:
1684
+ return []
1685
+ result = client.get_images(followup_state)
1686
+ # For images we simply return a list of original URLs
1687
+ return [item.get("original", "") for item in result]
1688
+
1689
+ def perform_video_search(followup_state):
1690
+ if not followup_state:
1691
+ return "<p>No followup ID available.</p>"
1692
+ result = client.get_videos(followup_state)
1693
+ # Use the helper to produce the embed iframes (supports multiple videos)
1694
+ return embed_video(result)
1695
+
1696
+ def perform_links_search(followup_state):
1697
+ if not followup_state:
1698
+ return gr.Markdown("No followup ID available.")
1699
+ result = client.get_links(followup_state)
1700
+ return format_links(result)
1701
+
1702
+ # ----------------------------
1703
+ # UI Build
1704
+ # ----------------------------
1705
+ css = """
1706
+ #chatbot {
1707
+ height: 100%;
1708
+ }
1709
+ h1, h2, h3, h4, h5, h6 {
1710
+ text-align: center;
1711
+ display: block;
1712
+ }
1713
+ """
1714
+
1715
+ # like chatgpt, but with less features. built by @theo and @r_marked
1716
+
1717
+ # defautl query: how to make slingshot?
1718
+ # who created light (e.g., electricity) Tesla or Edison in quick short?
1719
+ with gr.Blocks(css=css, fill_height=True) as demo:
1720
+ gr.Markdown("""
1721
+ ## like perplexity, but with less features.
1722
+ #### built by [@abdibrokhim](https://yaps.gg).
1723
+ """)
1724
+
1725
+ # State variables to hold followup ID and conversation history, plus follow-up questions text
1726
+ followup_state = gr.State(None)
1727
+ chat_history_state = gr.State([]) # holds conversation history as a list of messages
1728
+ followup_md_state = gr.State("") # holds follow-up questions as Markdown text
1729
+
1730
+ with gr.Row():
1731
+ with gr.Column(scale=3):
1732
+ with gr.Row():
1733
+ btn_local_map = gr.Button("Local Map Search (coming soon...)", variant="secondary", size="sm", interactive=False)
1734
+ btn_knowledge = gr.Button("Knowledge Base (coming soon...)", variant="secondary", size="sm", interactive=False)
1735
+ # The ChatInterface now uses additional outputs for both followup_state and conversation history,
1736
+ # plus follow-up questions Markdown.
1737
+ chat = gr.ChatInterface(
1738
+ fn=chat_function,
1739
+ type="messages",
1740
+ additional_inputs=[followup_state, chat_history_state],
1741
+ additional_outputs=[followup_state, chat_history_state, followup_md_state],
1742
+ )
1743
+ # Button callbacks to append local map and knowledge base results to chat
1744
+ btn_local_map.click(
1745
+ fn=handle_local_map_click,
1746
+ inputs=[followup_state, chat_history_state],
1747
+ outputs=chat.chatbot
1748
+ )
1749
+ btn_knowledge.click(
1750
+ fn=handle_knowledge_click,
1751
+ inputs=[followup_state, chat_history_state],
1752
+ outputs=chat.chatbot
1753
+ )
1754
+
1755
+ # Radio-based follow-up questions
1756
+ followup_radio = gr.Radio(
1757
+ choices=[],
1758
+ label="Follow-up Questions (select one and click 'Send Follow-up')"
1759
+ )
1760
+ btn_send_followup = gr.Button("Send Follow-up")
1761
+
1762
+ # When the user clicks "Send Follow-up", the selected question is passed
1763
+ # to handle_followup_click
1764
+ btn_send_followup.click(
1765
+ fn=handle_followup_click,
1766
+ inputs=[followup_radio, followup_state, chat_history_state],
1767
+ outputs=[chat.chatbot, followup_state, followup_md_state]
1768
+ )
1769
+
1770
+ # Update the radio choices when followup_md_state changes
1771
+ def update_followup_radio(md_text):
1772
+ """
1773
+ Parse Markdown lines to extract questions starting with '- '.
1774
+ """
1775
+ lines = md_text.splitlines()
1776
+ questions = []
1777
+ for line in lines:
1778
+ if line.startswith("- "):
1779
+ questions.append(line[2:])
1780
+ return gr.update(choices=questions, value=None)
1781
+
1782
+ followup_md_state.change(
1783
+ fn=update_followup_radio,
1784
+ inputs=[followup_md_state],
1785
+ outputs=[followup_radio]
1786
+ )
1787
+
1788
+ with gr.Column(scale=1):
1789
+ gr.Markdown("### Advanced Search Options")
1790
+ with gr.Column(variant="panel"):
1791
+ btn_images = gr.Button("Search Images")
1792
+ btn_videos = gr.Button("Search Videos")
1793
+ btn_links = gr.Button("Search Links")
1794
+ gallery_output = gr.Gallery(label="Image Results", columns=2)
1795
+ video_output = gr.HTML(label="Video Results") # HTML for embedded video iframes
1796
+ links_output = gr.Markdown(label="Links Results")
1797
+ btn_images.click(
1798
+ fn=perform_image_search,
1799
+ inputs=[followup_state],
1800
+ outputs=[gallery_output]
1801
+ )
1802
+ btn_videos.click(
1803
+ fn=perform_video_search,
1804
+ inputs=[followup_state],
1805
+ outputs=[video_output]
1806
+ )
1807
+ btn_links.click(
1808
+ fn=perform_links_search,
1809
+ inputs=[followup_state],
1810
+ outputs=[links_output]
1811
+ )
1812
+ demo.launch()
1813
+
1814
+ ```
1815
+ <|END|>
1816
+
1817
+ ----
1818
+
1819
+ [helpers.py]
1820
+ <|START|>
1821
+ ```
1822
+ from dotenv import load_dotenv
1823
+ import os
1824
+ import gradio as gr
1825
+ import urllib.parse
1826
+ import re
1827
+ from pytube import YouTube
1828
+ from typing import List, Optional, Dict
1829
+ from r_types import (
1830
+ SearchVideosResponse,
1831
+ SearchImagesResponse,
1832
+ SearchLinksResponse,
1833
+ LocalMapResponse,
1834
+ KnowledgeBaseResponse
1835
+ )
1836
+ import json
1837
+
1838
+
1839
+ def get_video_id(url: str) -> Optional[str]:
1840
+ """
1841
+ Safely retrieve the YouTube video_id from a given URL using pytube.
1842
+ Returns None if the URL is invalid or an error occurs.
1843
+ """
1844
+ if not url:
1845
+ return None
1846
+
1847
+ try:
1848
+ yt = YouTube(url)
1849
+ return yt.video_id
1850
+ except Exception:
1851
+ # If the URL is invalid or pytube fails, return None
1852
+ return None
1853
+
1854
+
1855
+ def embed_video(videos: List[SearchVideosResponse]) -> str:
1856
+ """
1857
+ Given a list of video data (with 'link' and 'title'),
1858
+ returns an HTML string of embedded YouTube iframes.
1859
+ """
1860
+ if not videos:
1861
+ return "<p>No videos found.</p>"
1862
+
1863
+ # Collect each iframe snippet
1864
+ iframes = []
1865
+ for video in videos:
1866
+ url = video.get("link", "")
1867
+ video_id = get_video_id(url)
1868
+ if not video_id:
1869
+ # Skip invalid or non-parsable links
1870
+ continue
1871
+
1872
+ title = video.get("title", "").replace('"', '\\"') # Escape quotes
1873
+ iframe = f"""
1874
+ <iframe
1875
+ width="560"
1876
+ height="315"
1877
+ src="https://www.youtube.com/embed/{video_id}"
1878
+ title="{title}"
1879
+ frameborder="0"
1880
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
1881
+ allowfullscreen>
1882
+ </iframe>
1883
+ """
1884
+ iframes.append(iframe)
1885
+
1886
+ # If no valid videos after processing, return a fallback message
1887
+ if not iframes:
1888
+ return "<p>No valid YouTube videos found.</p>"
1889
+
1890
+ # Join all iframes into one HTML string
1891
+ return "\n".join(iframes)
1892
+
1893
+ def get_video_thumbnail(videos: List[SearchVideosResponse]) -> str:
1894
+ pass
1895
+
1896
+ def format_links(links) -> str:
1897
+ """
1898
+ Convert a list of {'title': str, 'link': str} objects
1899
+ into a bulleted Markdown string with clickable links.
1900
+ """
1901
+ if not links:
1902
+ return "No links found."
1903
+
1904
+ links_md = "**Links:**\n"
1905
+ for url in links:
1906
+ title = url.rstrip('/').split('/')[-1]
1907
+ links_md += f"- [{title}]({url})\n"
1908
+ return links_md
1909
+
1910
+
1911
+ def embed_google_map(map_url: str) -> str:
1912
+ """
1913
+ Extracts a textual location from the given Google Maps URL
1914
+ and returns an embedded Google Map iframe for that location.
1915
+ Assumes you have a valid API key in place of 'YOUR_API_KEY'.
1916
+ """
1917
+ load_dotenv()
1918
+ GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY")
1919
+
1920
+ if not map_url:
1921
+ return "<p>Invalid Google Maps URL.</p>"
1922
+
1923
+ # Attempt to extract "San+Francisco,+CA" from the URL
1924
+ match = re.search(r"/maps/place/([^/]+)", map_url)
1925
+ if not match:
1926
+ return "Invalid Google Maps URL. Could not extract location."
1927
+
1928
+ location_text = match.group(1)
1929
+ # Remove query params or additional slashes from the captured group
1930
+ location_text = re.split(r"[/?]", location_text)[0]
1931
+
1932
+ # URL-encode location to avoid issues with special characters
1933
+ encoded_location = urllib.parse.quote(location_text, safe="")
1934
+
1935
+ embed_html = f"""
1936
+ <iframe
1937
+ width="600"
1938
+ height="450"
1939
+ style="border:0"
1940
+ loading="lazy"
1941
+ allowfullscreen
1942
+ src="https://www.google.com/maps/embed/v1/place?key={GOOGLE_MAPS_API_KEY}&q={encoded_location}">
1943
+ </iframe>
1944
+ """
1945
+ return embed_html
1946
+
1947
+
1948
+ def format_knowledge(raw_result: str) -> str:
1949
+ """
1950
+ Given a dictionary of knowledge data (e.g., about a person),
1951
+ produce a Markdown string summarizing that info.
1952
+ """
1953
+
1954
+ if not raw_result:
1955
+ return 0000
1956
+
1957
+ # Clean up the raw JSON string
1958
+ clean_json_str = cleanup_raw_json(raw_result)
1959
+ print('Knowledge Data: ', clean_json_str)
1960
+
1961
+ try:
1962
+ # Parse the cleaned JSON string
1963
+ result = json.loads(clean_json_str)
1964
+ title = result.get("title", "...")
1965
+ type_ = result.get("type", "...")
1966
+ born = result.get("born", "...")
1967
+ died = result.get("died", "...")
1968
+
1969
+ content = f"""
1970
+ **{title}**
1971
+ Type: {type_}
1972
+ Born: {born}
1973
+ Died: {died}
1974
+ """
1975
+ return content
1976
+ except json.JSONDecodeError:
1977
+ return "Error: Failed to parse knowledge data."
1978
+
1979
+
1980
+
1981
+ def format_followup_questions(raw_questions: str) -> str:
1982
+ """
1983
+ Extracts and formats follow-up questions from a raw JSON-like string.
1984
+
1985
+ The input string may contain triple backticks (```json ... ```) which need to be removed before parsing.
1986
+
1987
+ Expected input format:
1988
+ ```json
1989
+ {
1990
+ "followup_question": [
1991
+ "What materials are needed to make a slingshot?",
1992
+ "How to make a slingshot more powerful?"
1993
+ ]
1994
+ }
1995
+ ```
1996
+
1997
+ Returns a Markdown-formatted string with the follow-up questions.
1998
+ """
1999
+
2000
+ if not raw_questions:
2001
+ return "No follow-up questions available."
2002
+
2003
+ # Clean up the raw JSON string
2004
+ clean_json_str = cleanup_raw_json(raw_questions)
2005
+
2006
+ try:
2007
+ # Parse the cleaned JSON string
2008
+ questions_dict = json.loads(clean_json_str)
2009
+
2010
+ # Ensure the expected key exists
2011
+ followup_list = questions_dict.get("followup_question", [])
2012
+
2013
+ if not isinstance(followup_list, list) or not followup_list:
2014
+ return "No follow-up questions available."
2015
+
2016
+ # Format the questions into Markdown
2017
+ questions_md = "### Follow-up Questions\n\n"
2018
+ for question in followup_list:
2019
+ questions_md += f"- {question}\n"
2020
+
2021
+ return questions_md
2022
+
2023
+ except json.JSONDecodeError:
2024
+ return "Error: Failed to parse follow-up questions."
2025
+
2026
+ def cleanup_raw_json(raw_json: str) -> str:
2027
+ """
2028
+ Remove triple backticks and 'json' from the beginning and end of a raw JSON string.
2029
+ """
2030
+ return re.sub(r"```json|```", "", raw_json).strip()
2031
+ ```
2032
+ <|END|>
2033
+
2034
+ ----
2035
+
2036
+ [prompts.py]
2037
+ <|START|>
2038
+ ```
2039
+ SYSTEM_PROMPT_BASE = """<system_instructions for ai goes here. please skip this. i will complete it myself.>"""
2040
+ SYSTEM_PROMPT_MAP = """<system_instructions for ai goes here. please skip this. i will complete it myself.>"""
2041
+ SYSTEM_PROMPT_FOLLOWUP = """<system_instructions for ai goes here. please skip this. i will complete it myself.>"""
2042
+ SYSTEM_PROMPT_KNOWLEDGE_BASE = """<system_instructions for ai goes here. please skip this. i will complete it myself.>"""
2043
+ ```
2044
+ <|END|>
2045
+
2046
+ ----
2047
+
2048
+ [r_types.py]
2049
+ <|START|>
2050
+ ```
2051
+ ```
2052
+ <|END|>
2053
+
2054
+ ----
2055
+
2056
+ [requirements.txt]
2057
+ <|START|>
2058
+ ```
2059
+ openai
2060
+ gradio
2061
+ python-dotenv
2062
+ requests
2063
+ pytube
2064
+ ```
2065
+ <|END|>
2066
+
2067
+
2068
+ ----
2069
+
2070
+ [.gitignore]
2071
+ <|START|>
2072
+ ```
2073
+ .env
2074
+ .venv
2075
+ __pycache__
2076
+ *.pyc
2077
+ .DS_Store
2078
+ ```
2079
+ <|END|>
2080
+
2081
+
2082
+ ----
2083
+
2084
+ [.env]
2085
+ <|START|>
2086
+ ```
2087
+ AIML_API_KEY=...
2088
+ GOOGLE_MAPS_API_KEY=...
2089
+ ```
2090
+ <|END|>