Patelcoder commited on
Commit
1fd1980
Β·
verified Β·
1 Parent(s): ab6d766

Add index.html

Browse files
Files changed (1) hide show
  1. index.html +192 -0
index.html ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Story RAG Assistant (Gradio-Lite)</title>
7
+ <script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.js"></script>
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.css" />
9
+ <style>
10
+ body {
11
+ background-color: #0f172a;
12
+ color: #f8fafc;
13
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
14
+ margin: 0;
15
+ padding: 20px;
16
+ }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <gradio-lite>
21
+ <gradio-code>
22
+ import gradio as gr
23
+ import json
24
+ from pyodide.http import pyfetch
25
+
26
+ async def make_post_request(url, headers, body_dict):
27
+ response = await pyfetch(
28
+ url,
29
+ method="POST",
30
+ headers=headers,
31
+ body=json.dumps(body_dict)
32
+ )
33
+ if response.status != 200:
34
+ err_msg = await response.string()
35
+ raise Exception(f"Request failed with status {response.status}: {err_msg}")
36
+ return await response.json()
37
+
38
+ async def get_pinecone_host(api_key, index_name):
39
+ # Route control plane through corsproxy.io because Pinecone blocks browser CORS
40
+ url = f"https://corsproxy.io/?url=https://api.pinecone.io/indexes/{index_name}"
41
+ response = await pyfetch(
42
+ url,
43
+ method="GET",
44
+ headers={
45
+ "Api-Key": api_key,
46
+ "Accept": "application/json"
47
+ }
48
+ )
49
+ if response.status != 200:
50
+ err_msg = await response.string()
51
+ raise Exception(f"Failed to get Pinecone index info: {err_msg}")
52
+ data = await response.json()
53
+ return data["host"]
54
+
55
+ async def query_pinecone(api_key, host, namespace, text, top_k=4):
56
+ # Route data plane through corsproxy.io
57
+ url = f"https://corsproxy.io/?url=https://{host}/query"
58
+ headers = {
59
+ "Api-Key": api_key,
60
+ "Content-Type": "application/json",
61
+ "Accept": "application/json"
62
+ }
63
+ body = {
64
+ "namespace": namespace,
65
+ "topK": top_k,
66
+ "inputs": {
67
+ "text": text
68
+ },
69
+ "includeMetadata": True
70
+ }
71
+ data = await make_post_request(url, headers, body)
72
+ matches = data.get("matches", [])
73
+ chunks = [m["metadata"].get("chunk_text", "") for m in matches if "metadata" in m]
74
+ return chunks
75
+
76
+ async def call_gemini(api_key, model_name, contents):
77
+ # Gemini API natively supports CORS, so we do not need a proxy for it
78
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={api_key}"
79
+ headers = {
80
+ "Content-Type": "application/json"
81
+ }
82
+ body = {
83
+ "contents": contents,
84
+ "generationConfig": {
85
+ "temperature": 0.1
86
+ }
87
+ }
88
+ data = await make_post_request(url, headers, body)
89
+ try:
90
+ text = data["candidates"][0]["content"]["parts"][0]["text"]
91
+ return text
92
+ except (KeyError, IndexError):
93
+ raise Exception(f"Failed to parse Gemini response: {json.dumps(data)}")
94
+
95
+ async def respond(message, history, google_key, pinecone_key, pinecone_index, pinecone_ns, google_model):
96
+ if not google_key or not pinecone_key:
97
+ history.append({"role": "user", "content": message})
98
+ history.append({"role": "assistant", "content": "❌ Please provide both Google and Pinecone API keys in the configuration panel on the left."})
99
+ return "", history
100
+
101
+ try:
102
+ # 1. Fetch Pinecone Host
103
+ host = await get_pinecone_host(pinecone_key, pinecone_index)
104
+
105
+ # 2. Search Pinecone for context
106
+ chunks = await query_pinecone(pinecone_key, host, pinecone_ns, message)
107
+
108
+ # 3. Format prompt
109
+ sources_text = "\n\n".join(f"Source {i+1}:\n{text}" for i, text in enumerate(chunks)) if chunks else "No relevant sources found."
110
+
111
+ system_prompt = (
112
+ "You are an educational assistant answering questions about stories in uploaded documents.\n"
113
+ "Rules:\n"
114
+ "- Use ONLY facts from the retrieved sources. Never invent details.\n"
115
+ "- Give a direct answer in 2-4 sentences. Do not repeat the question or show your reasoning.\n"
116
+ "- If multiple stories appear, answer about the one most relevant to the question.\n"
117
+ "- If the sources do not contain the answer, say you could not find it in the document."
118
+ )
119
+
120
+ augmented_prompt = (
121
+ f"{system_prompt}\n\n"
122
+ f"Retrieved sources (answer using ONLY these):\n{sources_text}\n\n"
123
+ f"Question: {message}"
124
+ )
125
+
126
+ # 4. Construct conversation contents for Gemini
127
+ contents = []
128
+ for turn in history[-8:]:
129
+ role = "user" if turn["role"] == "user" else "model"
130
+ contents.append({
131
+ "role": role,
132
+ "parts": [{"text": turn["content"]}]
133
+ })
134
+ contents.append({
135
+ "role": "user",
136
+ "parts": [{"text": augmented_prompt}]
137
+ })
138
+
139
+ # 5. Call Gemini
140
+ response_text = await call_gemini(google_key, google_model, contents)
141
+
142
+ history.append({"role": "user", "content": message})
143
+ history.append({"role": "assistant", "content": response_text})
144
+ return "", history
145
+
146
+ except Exception as e:
147
+ history.append({"role": "user", "content": message})
148
+ history.append({"role": "assistant", "content": f"❌ Error: {str(e)}"})
149
+ return "", history
150
+
151
+ # Custom WebAssembly Theme and Styling
152
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="indigo")) as demo:
153
+ gr.Markdown("# πŸ“š Static RAG Story Assistant (Gradio-Lite)")
154
+ gr.Markdown(
155
+ "This application runs **entirely inside your browser** via WebAssembly (Pyodide). "
156
+ "No backend server is required, and your API keys never leave your browser session (except to call Pinecone and Gemini)."
157
+ )
158
+
159
+ with gr.Row():
160
+ with gr.Column(scale=1):
161
+ gr.Markdown("### πŸ”‘ API Configuration")
162
+ google_key_input = gr.Textbox(label="Google API Key", type="password", placeholder="AIzaSy...")
163
+ pinecone_key_input = gr.Textbox(label="Pinecone API Key", type="password", placeholder="pcsk_...")
164
+ pinecone_index_input = gr.Textbox(label="Pinecone Index Name", value="story-llama")
165
+ pinecone_ns_input = gr.Textbox(label="Pinecone Namespace", value="default")
166
+ google_model_input = gr.Textbox(label="Google Model", value="gemini-2.0-flash")
167
+
168
+ with gr.Column(scale=2):
169
+ chatbot = gr.Chatbot(type="messages", height=450)
170
+ msg_input = gr.Textbox(placeholder="Ask a question about the story...", label="Your Question")
171
+
172
+ with gr.Row():
173
+ submit_btn = gr.Button("Send", variant="primary")
174
+ clear_btn = gr.Button("Clear Chat")
175
+
176
+ submit_btn.click(
177
+ respond,
178
+ inputs=[msg_input, chatbot, google_key_input, pinecone_key_input, pinecone_index_input, pinecone_ns_input, google_model_input],
179
+ outputs=[msg_input, chatbot]
180
+ )
181
+ msg_input.submit(
182
+ respond,
183
+ inputs=[msg_input, chatbot, google_key_input, pinecone_key_input, pinecone_index_input, pinecone_ns_input, google_model_input],
184
+ outputs=[msg_input, chatbot]
185
+ )
186
+ clear_btn.click(lambda: [], None, chatbot)
187
+
188
+ demo.launch()
189
+ </gradio-code>
190
+ </gradio-lite>
191
+ </body>
192
+ </html>