Spaces:
Runtime error
Runtime error
Commit
·
21ffffb
1
Parent(s):
954241d
tons of changes
Browse files- app.py +144 -93
- bagoodex_client.py +17 -1
- helpers.py +190 -43
- prompts.py +46 -0
- r_types.py +83 -0
- test_api.py +21 -0
- test_app.py +41 -0
app.py
CHANGED
@@ -1,132 +1,183 @@
|
|
1 |
import os
|
2 |
-
import requests
|
3 |
import gradio as gr
|
4 |
-
from openai import OpenAI
|
5 |
-
from dotenv import load_dotenv
|
6 |
-
# from helpers import format_images, format_videos, format_links, format_local_map, format_knowledge
|
7 |
from bagoodex_client import BagoodexClient
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
client = BagoodexClient()
|
15 |
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
died = result.get('died', '')
|
21 |
-
content = f"""
|
22 |
-
**{title}**
|
23 |
-
Type: {type_}
|
24 |
-
Born: {born}
|
25 |
-
Died: {died}
|
26 |
"""
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
# Helper formatting functions
|
34 |
-
def format_videos(result):
|
35 |
-
return [vid.get('link', '') for vid in result]
|
36 |
-
|
37 |
-
# Advanced search functions
|
38 |
-
def perform_video_search(followup_id):
|
39 |
-
if not followup_id:
|
40 |
-
return []
|
41 |
-
result = client.get_videos(followup_id)
|
42 |
-
return format_videos(result)
|
43 |
-
|
44 |
-
def format_links(result):
|
45 |
-
links_md = "**Links:**\n"
|
46 |
-
for url in result:
|
47 |
-
title = url.rstrip('/').split('/')[-1]
|
48 |
-
links_md += f"- [{title}]({url})\n"
|
49 |
-
return gr.Markdown(links_md)
|
50 |
-
|
51 |
-
# Define the chat function
|
52 |
-
def chat_function(message, history, followup_id):
|
53 |
followup_id_new, answer = client.complete_chat(message)
|
54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
|
56 |
-
def
|
57 |
-
|
58 |
-
|
59 |
-
html = f"""
|
60 |
-
<div>
|
61 |
-
<strong>Local Map:</strong><br>
|
62 |
-
<a href='{link}' target='_blank'>View on Google Maps</a><br>
|
63 |
-
<img src='{image_url}' style='width:100%;'/>
|
64 |
-
</div>
|
65 |
"""
|
66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
|
68 |
-
def
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
76 |
-
def
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
|
|
|
|
|
|
83 |
|
84 |
-
#
|
85 |
-
|
86 |
-
|
|
|
|
|
87 |
return []
|
88 |
-
result = client.get_images(
|
89 |
-
|
90 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
|
92 |
-
def perform_links_search(
|
93 |
-
if not
|
94 |
return gr.Markdown("No followup ID available.")
|
95 |
-
result = client.get_links(
|
96 |
return format_links(result)
|
97 |
|
98 |
-
#
|
|
|
|
|
99 |
css = """
|
100 |
#chatbot {
|
101 |
height: 100%;
|
102 |
}
|
103 |
"""
|
104 |
|
105 |
-
# Build UI
|
106 |
with gr.Blocks(css=css, fill_height=True) as demo:
|
|
|
107 |
followup_state = gr.State(None)
|
|
|
|
|
|
|
108 |
with gr.Row():
|
109 |
with gr.Column(scale=3):
|
110 |
with gr.Row():
|
111 |
btn_local_map = gr.Button("Local Map Search", variant="secondary", size="sm")
|
112 |
btn_knowledge = gr.Button("Knowledge Base", variant="secondary", size="sm")
|
|
|
|
|
113 |
chat = gr.ChatInterface(
|
114 |
fn=chat_function,
|
115 |
type="messages",
|
116 |
-
additional_inputs=[followup_state],
|
117 |
-
additional_outputs=[followup_state],
|
118 |
)
|
119 |
-
#
|
120 |
btn_local_map.click(
|
121 |
-
|
122 |
-
inputs=[followup_state,
|
123 |
outputs=chat.chatbot
|
124 |
)
|
125 |
btn_knowledge.click(
|
126 |
-
|
127 |
-
inputs=[followup_state,
|
128 |
outputs=chat.chatbot
|
129 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
with gr.Column(scale=1):
|
131 |
gr.Markdown("### Advanced Search Options")
|
132 |
with gr.Column(variant="panel"):
|
@@ -134,21 +185,21 @@ with gr.Blocks(css=css, fill_height=True) as demo:
|
|
134 |
btn_videos = gr.Button("Search Videos")
|
135 |
btn_links = gr.Button("Search Links")
|
136 |
gallery_output = gr.Gallery(label="Image Results", columns=2)
|
137 |
-
video_output = gr.
|
138 |
links_output = gr.Markdown(label="Links Results")
|
139 |
btn_images.click(
|
140 |
-
perform_image_search,
|
141 |
inputs=[followup_state],
|
142 |
outputs=[gallery_output]
|
143 |
)
|
144 |
btn_videos.click(
|
145 |
-
perform_video_search,
|
146 |
inputs=[followup_state],
|
147 |
outputs=[video_output]
|
148 |
)
|
149 |
btn_links.click(
|
150 |
-
perform_links_search,
|
151 |
inputs=[followup_state],
|
152 |
outputs=[links_output]
|
153 |
)
|
154 |
-
demo.launch()
|
|
|
1 |
import os
|
|
|
2 |
import gradio as gr
|
|
|
|
|
|
|
3 |
from bagoodex_client import BagoodexClient
|
4 |
+
from r_types import ChatMessage
|
5 |
+
from prompts import SYSTEM_PROMPT_FOLLOWUP, SYSTEM_PROMPT_MAP, SYSTEM_PROMPT_BASE
|
6 |
+
from helpers import (
|
7 |
+
embed_video,
|
8 |
+
embed_image,
|
9 |
+
format_links,
|
10 |
+
embed_google_map,
|
11 |
+
format_knowledge,
|
12 |
+
format_followup_questions
|
13 |
+
)
|
14 |
|
15 |
client = BagoodexClient()
|
16 |
|
17 |
+
# ----------------------------
|
18 |
+
# Chat & Follow-up Functions
|
19 |
+
# ----------------------------
|
20 |
+
def chat_function(message, history, followup_state, chat_history_state):
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
"""
|
22 |
+
Process a new user message.
|
23 |
+
Appends the message and response to the conversation,
|
24 |
+
and retrieves follow-up questions.
|
25 |
+
"""
|
26 |
+
# complete_chat returns a new followup id and answer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
followup_id_new, answer = client.complete_chat(message)
|
28 |
+
# Update conversation history (if history is None, use an empty list)
|
29 |
+
if history is None:
|
30 |
+
history = []
|
31 |
+
updated_history = history + [ChatMessage({"role": "user", "content": message}),
|
32 |
+
ChatMessage({"role": "assistant", "content": answer})]
|
33 |
+
# Retrieve follow-up questions using the updated conversation
|
34 |
+
followup_questions_raw = client.base_qna(
|
35 |
+
messages=updated_history, system_prompt=SYSTEM_PROMPT_FOLLOWUP
|
36 |
+
)
|
37 |
+
# Format them using the helper
|
38 |
+
followup_md = format_followup_questions(followup_questions_raw)
|
39 |
+
return answer, followup_id_new, updated_history, followup_md
|
40 |
|
41 |
+
def handle_followup_click(question, followup_state, chat_history_state):
|
42 |
+
"""
|
43 |
+
When a follow-up question is clicked, send it as a new message.
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
"""
|
45 |
+
if not question:
|
46 |
+
return chat_history_state, followup_state, ""
|
47 |
+
# Process the follow-up question via complete_chat
|
48 |
+
followup_id_new, answer = client.complete_chat(question)
|
49 |
+
updated_history = chat_history_state + [ChatMessage({"role": "user", "content": question}),
|
50 |
+
ChatMessage({"role": "assistant", "content": answer})]
|
51 |
+
# Get new follow-up questions
|
52 |
+
followup_questions_raw = client.base_qna(
|
53 |
+
messages=updated_history, system_prompt=SYSTEM_PROMPT_FOLLOWUP
|
54 |
+
)
|
55 |
+
followup_md = format_followup_questions(followup_questions_raw)
|
56 |
+
return updated_history, followup_id_new, followup_md
|
57 |
|
58 |
+
def handle_local_map_click(followup_state, chat_history_state):
|
59 |
+
"""
|
60 |
+
On local map click, try to get a local map.
|
61 |
+
If issues occur, fall back to using the SYSTEM_PROMPT_MAP.
|
62 |
+
"""
|
63 |
+
if not followup_state:
|
64 |
+
return chat_history_state
|
65 |
+
try:
|
66 |
+
result = client.get_local_map(followup_state)
|
67 |
+
map_url = result.get('link', '')
|
68 |
+
# Use helper to produce an embedded map iframe
|
69 |
+
html = embed_google_map(map_url)
|
70 |
+
except Exception:
|
71 |
+
# Fall back: use the base_qna call with SYSTEM_PROMPT_MAP
|
72 |
+
result = client.base_qna(
|
73 |
+
messages=chat_history_state, system_prompt=SYSTEM_PROMPT_MAP
|
74 |
+
)
|
75 |
+
# Assume result contains a 'link' field
|
76 |
+
html = embed_google_map(result.get('link', ''))
|
77 |
+
new_message = ChatMessage({"role": "assistant", "content": html})
|
78 |
+
return chat_history_state + [new_message]
|
79 |
|
80 |
+
def handle_knowledge_click(followup_state, chat_history_state):
|
81 |
+
"""
|
82 |
+
On knowledge base click, fetch and format knowledge content.
|
83 |
+
"""
|
84 |
+
if not followup_state:
|
85 |
+
return chat_history_state
|
86 |
+
result = client.get_knowledge(followup_state)
|
87 |
+
md = format_knowledge(result)
|
88 |
+
new_message = ChatMessage({"role": "assistant", "content": md})
|
89 |
+
return chat_history_state + [new_message]
|
90 |
|
91 |
+
# ----------------------------
|
92 |
+
# Advanced Search Functions
|
93 |
+
# ----------------------------
|
94 |
+
def perform_image_search(followup_state):
|
95 |
+
if not followup_state:
|
96 |
return []
|
97 |
+
result = client.get_images(followup_state)
|
98 |
+
# For images we simply return a list of original URLs
|
99 |
+
return [item.get("original", "") for item in result]
|
100 |
+
|
101 |
+
def perform_video_search(followup_state):
|
102 |
+
if not followup_state:
|
103 |
+
return "<p>No followup ID available.</p>"
|
104 |
+
result = client.get_videos(followup_state)
|
105 |
+
# Use the helper to produce the embed iframes (supports multiple videos)
|
106 |
+
return embed_video(result)
|
107 |
|
108 |
+
def perform_links_search(followup_state):
|
109 |
+
if not followup_state:
|
110 |
return gr.Markdown("No followup ID available.")
|
111 |
+
result = client.get_links(followup_state)
|
112 |
return format_links(result)
|
113 |
|
114 |
+
# ----------------------------
|
115 |
+
# UI Build
|
116 |
+
# ----------------------------
|
117 |
css = """
|
118 |
#chatbot {
|
119 |
height: 100%;
|
120 |
}
|
121 |
"""
|
122 |
|
|
|
123 |
with gr.Blocks(css=css, fill_height=True) as demo:
|
124 |
+
# State variables to hold followup ID and conversation history, plus follow-up questions text
|
125 |
followup_state = gr.State(None)
|
126 |
+
chat_history_state = gr.State([]) # holds conversation history as a list of messages
|
127 |
+
followup_md_state = gr.State("") # holds follow-up questions as Markdown text
|
128 |
+
|
129 |
with gr.Row():
|
130 |
with gr.Column(scale=3):
|
131 |
with gr.Row():
|
132 |
btn_local_map = gr.Button("Local Map Search", variant="secondary", size="sm")
|
133 |
btn_knowledge = gr.Button("Knowledge Base", variant="secondary", size="sm")
|
134 |
+
# The ChatInterface now uses additional outputs for both followup_state and conversation history,
|
135 |
+
# plus follow-up questions Markdown.
|
136 |
chat = gr.ChatInterface(
|
137 |
fn=chat_function,
|
138 |
type="messages",
|
139 |
+
additional_inputs=[followup_state, chat_history_state],
|
140 |
+
additional_outputs=[followup_state, chat_history_state, followup_md_state],
|
141 |
)
|
142 |
+
# Button callbacks to append local map and knowledge base results to chat
|
143 |
btn_local_map.click(
|
144 |
+
fn=handle_local_map_click,
|
145 |
+
inputs=[followup_state, chat_history_state],
|
146 |
outputs=chat.chatbot
|
147 |
)
|
148 |
btn_knowledge.click(
|
149 |
+
fn=handle_knowledge_click,
|
150 |
+
inputs=[followup_state, chat_history_state],
|
151 |
outputs=chat.chatbot
|
152 |
)
|
153 |
+
# Below the chat input, display follow-up questions and let user select one.
|
154 |
+
followup_radio = gr.Radio(
|
155 |
+
choices=[], label="Follow-up Questions (select one and click Send Follow-up)"
|
156 |
+
)
|
157 |
+
btn_send_followup = gr.Button("Send Follow-up")
|
158 |
+
# When a follow-up question is sent, update the chat conversation, followup state, and follow-up list.
|
159 |
+
btn_send_followup.click(
|
160 |
+
fn=handle_followup_click,
|
161 |
+
inputs=[followup_radio, followup_state, chat_history_state],
|
162 |
+
outputs=[chat.chatbot, followup_state, followup_md_state]
|
163 |
+
)
|
164 |
+
# Also display the follow-up questions markdown (for reference) in a Markdown component.
|
165 |
+
followup_markdown = gr.Markdown(label="Follow-up Questions", value="", visible=True)
|
166 |
+
# When the followup_md_state updates, also update the radio choices.
|
167 |
+
def update_followup_radio(md_text):
|
168 |
+
# Assume the helper output is a Markdown string with list items.
|
169 |
+
# We split the text to extract the question lines.
|
170 |
+
lines = md_text.splitlines()
|
171 |
+
questions = []
|
172 |
+
for line in lines:
|
173 |
+
if line.startswith("- "):
|
174 |
+
questions.append(line[2:])
|
175 |
+
return gr.update(choices=questions, value=None), md_text
|
176 |
+
followup_md_state.change(
|
177 |
+
fn=update_followup_radio,
|
178 |
+
inputs=[followup_md_state],
|
179 |
+
outputs=[followup_radio, followup_markdown]
|
180 |
+
)
|
181 |
with gr.Column(scale=1):
|
182 |
gr.Markdown("### Advanced Search Options")
|
183 |
with gr.Column(variant="panel"):
|
|
|
185 |
btn_videos = gr.Button("Search Videos")
|
186 |
btn_links = gr.Button("Search Links")
|
187 |
gallery_output = gr.Gallery(label="Image Results", columns=2)
|
188 |
+
video_output = gr.HTML(label="Video Results") # HTML for embedded video iframes
|
189 |
links_output = gr.Markdown(label="Links Results")
|
190 |
btn_images.click(
|
191 |
+
fn=perform_image_search,
|
192 |
inputs=[followup_state],
|
193 |
outputs=[gallery_output]
|
194 |
)
|
195 |
btn_videos.click(
|
196 |
+
fn=perform_video_search,
|
197 |
inputs=[followup_state],
|
198 |
outputs=[video_output]
|
199 |
)
|
200 |
btn_links.click(
|
201 |
+
fn=perform_links_search,
|
202 |
inputs=[followup_state],
|
203 |
outputs=[links_output]
|
204 |
)
|
205 |
+
demo.launch()
|
bagoodex_client.py
CHANGED
@@ -2,6 +2,9 @@ import os
|
|
2 |
import requests
|
3 |
from openai import OpenAI
|
4 |
from dotenv import load_dotenv
|
|
|
|
|
|
|
5 |
|
6 |
load_dotenv()
|
7 |
API_KEY = os.getenv("AIML_API_KEY")
|
@@ -20,11 +23,24 @@ class BagoodexClient:
|
|
20 |
"""
|
21 |
response = self.client.chat.completions.create(
|
22 |
model="bagoodex/bagoodex-search-v1",
|
23 |
-
messages=[
|
|
|
|
|
|
|
24 |
)
|
25 |
followup_id = response.id # the unique ID for follow-up searches
|
26 |
answer = response.choices[0].message.content
|
27 |
return followup_id, answer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
|
29 |
def get_links(self, followup_id):
|
30 |
headers = {"Authorization": f"Bearer {self.api_key}"}
|
|
|
2 |
import requests
|
3 |
from openai import OpenAI
|
4 |
from dotenv import load_dotenv
|
5 |
+
from r_types import ChatMessage
|
6 |
+
from prompts import SYSTEM_PROMPT_BASE, SYSTEM_PROMPT_MAP
|
7 |
+
from typing import List
|
8 |
|
9 |
load_dotenv()
|
10 |
API_KEY = os.getenv("AIML_API_KEY")
|
|
|
23 |
"""
|
24 |
response = self.client.chat.completions.create(
|
25 |
model="bagoodex/bagoodex-search-v1",
|
26 |
+
messages=[
|
27 |
+
ChatMessage(role="user", content=SYSTEM_PROMPT_BASE),
|
28 |
+
ChatMessage(role="user", content=query)
|
29 |
+
],
|
30 |
)
|
31 |
followup_id = response.id # the unique ID for follow-up searches
|
32 |
answer = response.choices[0].message.content
|
33 |
return followup_id, answer
|
34 |
+
|
35 |
+
def base_qna(self, messages: List[ChatMessage], system_prompt=SYSTEM_PROMPT_BASE):
|
36 |
+
response = self.client.chat.completions.create(
|
37 |
+
model="gpt-4o",
|
38 |
+
messages=[
|
39 |
+
ChatMessage(role="user", content=system_prompt),
|
40 |
+
*messages
|
41 |
+
],
|
42 |
+
)
|
43 |
+
return response.choices[0].message.content
|
44 |
|
45 |
def get_links(self, followup_id):
|
46 |
headers = {"Authorization": f"Bearer {self.api_key}"}
|
helpers.py
CHANGED
@@ -1,43 +1,190 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dotenv import load_dotenv
|
2 |
+
import os
|
3 |
+
import gradio as gr
|
4 |
+
import urllib.parse
|
5 |
+
import re
|
6 |
+
from pytube import YouTube
|
7 |
+
from typing import List, Optional
|
8 |
+
from r_types import (
|
9 |
+
SearchVideosResponse,
|
10 |
+
SearchImagesResponse,
|
11 |
+
SearchLinksResponse,
|
12 |
+
LocalMapResponse,
|
13 |
+
KnowledgeBaseResponse
|
14 |
+
)
|
15 |
+
|
16 |
+
|
17 |
+
def get_video_id(url: str) -> Optional[str]:
|
18 |
+
"""
|
19 |
+
Safely retrieve the YouTube video_id from a given URL using pytube.
|
20 |
+
Returns None if the URL is invalid or an error occurs.
|
21 |
+
"""
|
22 |
+
if not url:
|
23 |
+
return None
|
24 |
+
|
25 |
+
try:
|
26 |
+
yt = YouTube(url)
|
27 |
+
return yt.video_id
|
28 |
+
except Exception:
|
29 |
+
# If the URL is invalid or pytube fails, return None
|
30 |
+
return None
|
31 |
+
|
32 |
+
|
33 |
+
def embed_video(videos: List[SearchVideosResponse]) -> str:
|
34 |
+
"""
|
35 |
+
Given a list of video data (with 'link' and 'title'),
|
36 |
+
returns an HTML string of embedded YouTube iframes.
|
37 |
+
"""
|
38 |
+
if not videos:
|
39 |
+
return "<p>No videos found.</p>"
|
40 |
+
|
41 |
+
# Collect each iframe snippet
|
42 |
+
iframes = []
|
43 |
+
for video in videos:
|
44 |
+
url = video.get("link", "")
|
45 |
+
video_id = get_video_id(url)
|
46 |
+
if not video_id:
|
47 |
+
# Skip invalid or non-parsable links
|
48 |
+
continue
|
49 |
+
|
50 |
+
title = video.get("title", "").replace('"', '\\"') # Escape quotes
|
51 |
+
iframe = f"""
|
52 |
+
<iframe
|
53 |
+
width="560"
|
54 |
+
height="315"
|
55 |
+
src="https://www.youtube.com/embed/{video_id}"
|
56 |
+
title="{title}"
|
57 |
+
frameborder="0"
|
58 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
59 |
+
allowfullscreen>
|
60 |
+
</iframe>
|
61 |
+
"""
|
62 |
+
iframes.append(iframe)
|
63 |
+
|
64 |
+
# If no valid videos after processing, return a fallback message
|
65 |
+
if not iframes:
|
66 |
+
return "<p>No valid YouTube videos found.</p>"
|
67 |
+
|
68 |
+
# Join all iframes into one HTML string
|
69 |
+
return "\n".join(iframes)
|
70 |
+
|
71 |
+
|
72 |
+
def embed_image(json_data: SearchImagesResponse) -> str:
|
73 |
+
"""
|
74 |
+
Given image data with 'original' (URL) and 'title',
|
75 |
+
returns an HTML string with an <img> tag.
|
76 |
+
"""
|
77 |
+
title = json_data.get("title", "").replace('"', '\\"')
|
78 |
+
original = json_data.get("original", "")
|
79 |
+
|
80 |
+
if not original:
|
81 |
+
return "<p>No image URL provided.</p>"
|
82 |
+
|
83 |
+
embed_html = f"""
|
84 |
+
<img src="{original}" alt="{title}" style="width:100%">
|
85 |
+
"""
|
86 |
+
return embed_html
|
87 |
+
|
88 |
+
|
89 |
+
def build_search_links_response(urls: List[str]) -> List[SearchLinksResponse]:
|
90 |
+
"""
|
91 |
+
Convert raw URLs into a list of dicts,
|
92 |
+
each with 'title' and 'link' keys for display.
|
93 |
+
"""
|
94 |
+
results = []
|
95 |
+
for url in urls:
|
96 |
+
# Extract the last part of the URL as a rough "title"
|
97 |
+
raw_title = url.rstrip("/").split("/")[-1]
|
98 |
+
|
99 |
+
# Decode URL-encoded entities like %20
|
100 |
+
decoded_title = urllib.parse.unquote(raw_title)
|
101 |
+
|
102 |
+
# Replace hyphens/underscores with spaces
|
103 |
+
nice_title = decoded_title.replace("_", " ").replace("-", " ")
|
104 |
+
|
105 |
+
results.append({"title": nice_title, "link": url})
|
106 |
+
return results
|
107 |
+
|
108 |
+
|
109 |
+
def format_links(links: List[SearchLinksResponse]) -> str:
|
110 |
+
"""
|
111 |
+
Convert a list of {'title': str, 'link': str} objects
|
112 |
+
into a bulleted Markdown string with clickable links.
|
113 |
+
"""
|
114 |
+
if not links:
|
115 |
+
return "No links found."
|
116 |
+
|
117 |
+
links_md = "### Links\n\n"
|
118 |
+
for item in links:
|
119 |
+
links_md += f"- [{item['title']}]({item['link']})\n"
|
120 |
+
return links_md
|
121 |
+
|
122 |
+
|
123 |
+
def embed_google_map(map_url: str) -> str:
|
124 |
+
"""
|
125 |
+
Extracts a textual location from the given Google Maps URL
|
126 |
+
and returns an embedded Google Map iframe for that location.
|
127 |
+
Assumes you have a valid API key in place of 'YOUR_API_KEY'.
|
128 |
+
"""
|
129 |
+
load_dotenv()
|
130 |
+
GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY")
|
131 |
+
|
132 |
+
if not map_url:
|
133 |
+
return "<p>Invalid Google Maps URL.</p>"
|
134 |
+
|
135 |
+
# Attempt to extract "San+Francisco,+CA" from the URL
|
136 |
+
match = re.search(r"/maps/place/([^/]+)", map_url)
|
137 |
+
if not match:
|
138 |
+
return "Invalid Google Maps URL. Could not extract location."
|
139 |
+
|
140 |
+
location_text = match.group(1)
|
141 |
+
# Remove query params or additional slashes from the captured group
|
142 |
+
location_text = re.split(r"[/?]", location_text)[0]
|
143 |
+
|
144 |
+
# URL-encode location to avoid issues with special characters
|
145 |
+
encoded_location = urllib.parse.quote(location_text, safe="")
|
146 |
+
|
147 |
+
embed_html = f"""
|
148 |
+
<iframe
|
149 |
+
width="600"
|
150 |
+
height="450"
|
151 |
+
style="border:0"
|
152 |
+
loading="lazy"
|
153 |
+
allowfullscreen
|
154 |
+
src="https://www.google.com/maps/embed/v1/place?key={GOOGLE_MAPS_API_KEY}&q={encoded_location}">
|
155 |
+
</iframe>
|
156 |
+
"""
|
157 |
+
return embed_html
|
158 |
+
|
159 |
+
|
160 |
+
def format_knowledge(result: KnowledgeBaseResponse) -> str:
|
161 |
+
"""
|
162 |
+
Given a dictionary of knowledge data (e.g., about a person),
|
163 |
+
produce a Markdown string summarizing that info.
|
164 |
+
"""
|
165 |
+
title = result.get("title", "Unknown")
|
166 |
+
type_ = result.get("type", "")
|
167 |
+
born = result.get("born", "")
|
168 |
+
died = result.get("died", "")
|
169 |
+
|
170 |
+
content = f"""
|
171 |
+
**{title}**
|
172 |
+
Type: {type_}
|
173 |
+
Born: {born}
|
174 |
+
Died: {died}
|
175 |
+
"""
|
176 |
+
return content
|
177 |
+
|
178 |
+
|
179 |
+
def format_followup_questions(questions: List[str]) -> str:
|
180 |
+
"""
|
181 |
+
Given a list of follow-up questions, return a Markdown string
|
182 |
+
with each question as a bulleted list item.
|
183 |
+
"""
|
184 |
+
if not questions:
|
185 |
+
return "No follow-up questions provided."
|
186 |
+
|
187 |
+
questions_md = "### Follow-up Questions\n\n"
|
188 |
+
for question in questions:
|
189 |
+
questions_md += f"- {question}\n"
|
190 |
+
return questions_md
|
prompts.py
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
SYSTEM_PROMPT_BASE = """
|
2 |
+
######SYSTEM INIATED######
|
3 |
+
You will be given a conversation chat (e.g., text/ paragraph).
|
4 |
+
Answer the given conversation chat with a relevant response.
|
5 |
+
|
6 |
+
######NOTE######
|
7 |
+
Be nice and polite in your responses!
|
8 |
+
######SYSTEM SHUTDOWN######
|
9 |
+
"""
|
10 |
+
|
11 |
+
SYSTEM_PROMPT_MAP = """
|
12 |
+
######SYSTEM INIATED######
|
13 |
+
You will be given a content from conversation chat (e.g., text/ paragraph).
|
14 |
+
Your task is to analyze the given content and provide different types of places as close as possible to the given content.
|
15 |
+
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.
|
16 |
+
Make sure the places you provide are relevant to the given content. And as much as close to the given content, the better.
|
17 |
+
Your final output should be a list of places.
|
18 |
+
Here's JSON format example:
|
19 |
+
```json
|
20 |
+
{
|
21 |
+
"places": ["Hardware store", "Woodworking shop", "Outdoor sports store"]
|
22 |
+
}
|
23 |
+
```
|
24 |
+
######NOTE######
|
25 |
+
Make sure to return only JSON data! Nothing else!
|
26 |
+
######SYSTEM SHUTDOWN######
|
27 |
+
"""
|
28 |
+
|
29 |
+
|
30 |
+
SYSTEM_PROMPT_FOLLOWUP = """
|
31 |
+
######SYSTEM INIATED######
|
32 |
+
You will be given a content from conversation chat (e.g., text/ paragraph).
|
33 |
+
Your task is to analyze the given content and provide a follow-up question based on the given content.
|
34 |
+
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?".
|
35 |
+
Make sure the follow-up question you provide is relevant to the given content.
|
36 |
+
Your final output should be a List of follow-up question.
|
37 |
+
Here's JSON format example:
|
38 |
+
```json
|
39 |
+
{
|
40 |
+
"followup_question": ["What materials are needed to make a slingshot?", "How to make a slingshot more powerful?"]
|
41 |
+
}
|
42 |
+
```
|
43 |
+
######NOTE######
|
44 |
+
Make sure to return only JSON data! Nothing else!
|
45 |
+
######SYSTEM SHUTDOWN######
|
46 |
+
"""
|
r_types.py
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import TypedDict
|
2 |
+
|
3 |
+
# [ChatMessage]:
|
4 |
+
# <response>
|
5 |
+
# {
|
6 |
+
# "role": "system",
|
7 |
+
# "content": "Hello, how can I help you today?"
|
8 |
+
# }
|
9 |
+
# </response>
|
10 |
+
|
11 |
+
class ChatMessage(TypedDict):
|
12 |
+
role: str
|
13 |
+
content: str
|
14 |
+
|
15 |
+
|
16 |
+
# [Search Videos]:
|
17 |
+
# <response>
|
18 |
+
# Videos:
|
19 |
+
# [{'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'}]
|
20 |
+
# </response>
|
21 |
+
|
22 |
+
class SearchVideosResponse(TypedDict):
|
23 |
+
link: str
|
24 |
+
thumbnail: str
|
25 |
+
title: str
|
26 |
+
|
27 |
+
|
28 |
+
# [Search Images]:
|
29 |
+
# <response>
|
30 |
+
# [{'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'}]
|
31 |
+
# </response>
|
32 |
+
|
33 |
+
class SearchImagesResponse(TypedDict):
|
34 |
+
source: str
|
35 |
+
original: str
|
36 |
+
title: str
|
37 |
+
source: str
|
38 |
+
source_name: str
|
39 |
+
|
40 |
+
|
41 |
+
# [Links]:
|
42 |
+
# <response>
|
43 |
+
# ['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/']
|
44 |
+
# </response>
|
45 |
+
|
46 |
+
class SearchLinksResponse(TypedDict):
|
47 |
+
title: str
|
48 |
+
link: str
|
49 |
+
|
50 |
+
|
51 |
+
### Local Map Response:
|
52 |
+
# <response>
|
53 |
+
# {
|
54 |
+
# "link": "https://www.google.com/maps/place/San+Francisco,+CA/data=!4m2!3m1!1s0x80859a6d00690021:0x4a501367f076adff?sa=X&ved=2ahUKEwjqg7eNz9KLAxVCFFkFHWSPEeIQ8gF6BAgqEAA&hl=en",
|
55 |
+
# "image": "https://dmwtgq8yidg0m.cloudfront.net/images/TdNFUpcEvvHL-local-map.webp"
|
56 |
+
# }
|
57 |
+
# </response>
|
58 |
+
|
59 |
+
class LocalMapResponse(TypedDict):
|
60 |
+
link: str
|
61 |
+
imgae: str
|
62 |
+
|
63 |
+
|
64 |
+
### Model Response:
|
65 |
+
|
66 |
+
# <response>
|
67 |
+
# ```
|
68 |
+
# {
|
69 |
+
# 'title': 'Nikola Tesla',
|
70 |
+
# 'type': 'Engineer and futurist',
|
71 |
+
# 'description': None,
|
72 |
+
# 'born': 'July 10, 1856, Smiljan, Croatia',
|
73 |
+
# 'died': 'January 7, 1943 (age 86 years), The New Yorker A Wyndham Hotel, New York, NY'
|
74 |
+
# }
|
75 |
+
# ```
|
76 |
+
# </response>
|
77 |
+
|
78 |
+
class KnowledgeBaseResponse(TypedDict):
|
79 |
+
title: str
|
80 |
+
type: str
|
81 |
+
description: str
|
82 |
+
born: str
|
83 |
+
died: str
|
test_api.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from openai import OpenAI
|
3 |
+
|
4 |
+
client = OpenAI(
|
5 |
+
base_url="https://api.aimlapi.com/v1",
|
6 |
+
api_key=os.getenv("AIML_API_KEY"),
|
7 |
+
)
|
8 |
+
|
9 |
+
response = client.chat.completions.create(
|
10 |
+
model="gpt-4o",
|
11 |
+
messages=[
|
12 |
+
{
|
13 |
+
"role": "user",
|
14 |
+
"content": "Tell me, why is the sky blue?"
|
15 |
+
},
|
16 |
+
],
|
17 |
+
)
|
18 |
+
|
19 |
+
message = response.choices[0].message.content
|
20 |
+
|
21 |
+
print(f"Assistant: {message}")
|
test_app.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import re
|
3 |
+
|
4 |
+
def embed_google_map(map_url):
|
5 |
+
"""
|
6 |
+
Extracts a textual location from the Google Maps URL and returns
|
7 |
+
an embedded Google Map iframe.
|
8 |
+
"""
|
9 |
+
# Simple approach: try to extract "San+Francisco,+CA" from the URL
|
10 |
+
# by capturing what's after '/maps/place/'
|
11 |
+
match = re.search(r'/maps/place/([^/]+)', map_url)
|
12 |
+
if not match:
|
13 |
+
return "Invalid Google Maps URL. Could not extract location."
|
14 |
+
|
15 |
+
location_text = match.group(1)
|
16 |
+
# location_text might contain extra parameters, so let's just
|
17 |
+
# strip everything after the first '?' or '/':
|
18 |
+
location_text = re.split(r'[/?]', location_text)[0]
|
19 |
+
|
20 |
+
embed_html = f"""
|
21 |
+
<iframe
|
22 |
+
width="600"
|
23 |
+
height="450"
|
24 |
+
style="border:0"
|
25 |
+
loading="lazy"
|
26 |
+
allowfullscreen
|
27 |
+
src="https://www.google.com/maps/embed/v1/place?key=YOUR_API_KEY&q={location_text}">
|
28 |
+
</iframe>
|
29 |
+
"""
|
30 |
+
return embed_html
|
31 |
+
|
32 |
+
with gr.Blocks() as demo:
|
33 |
+
map_url_input = gr.Textbox(label="Enter Google Maps URL")
|
34 |
+
map_output = gr.HTML(label="Embedded Google Map")
|
35 |
+
|
36 |
+
map_url_input.change(embed_google_map, inputs=map_url_input, outputs=map_output)
|
37 |
+
|
38 |
+
demo.launch()
|
39 |
+
|
40 |
+
|
41 |
+
# <iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d11988.613116381091!2d69.19850824650604!3d41.3055290002301!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x38ae8bc5b351bc23%3A0x707af898bf0cdb4d!2sEsenin%20St%2018%2C%20Tashkent%2C%20Uzbekistan!5e0!3m2!1sen!2s!4v1740469397909!5m2!1sen!2s" width="600" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
|