Tanuj commited on
Commit
834bb8b
·
1 Parent(s): 2cf474e

add new gradio + agent implementation

Browse files
README.md CHANGED
@@ -10,4 +10,72 @@ pinned: false
10
  license: mit
11
  ---
12
 
13
- An example chatbot using [Gradio](https://gradio.app), [`huggingface_hub`](https://huggingface.co/docs/huggingface_hub/v0.22.2/en/index), and the [Hugging Face Inference API](https://huggingface.co/docs/api-inference/index).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  license: mit
11
  ---
12
 
13
+ An example chatbot using [Gradio](https://gradio.app), [`huggingface_hub`](https://huggingface.co/docs/huggingface_hub/v0.22.2/en/index), and the [Hugging Face Inference API](https://huggingface.co/docs/api-inference/index).
14
+
15
+ # YouTwo Agent - Modal Deployment Guide
16
+
17
+ This repository contains the YouTwo Agent, a conversational AI agent powered by FastRTC for real-time audio communication.
18
+
19
+ ## Prerequisites
20
+
21
+ 1. Install the Modal CLI:
22
+ ```bash
23
+ pip install modal
24
+ ```
25
+
26
+ 2. Authenticate with Modal:
27
+ ```bash
28
+ modal token new
29
+ ```
30
+
31
+ 3. Create the following secrets and volumes on Modal:
32
+ - Secret: `youtwo-secrets` with the following values:
33
+ - `NEBIUS_API_KEY` - Your Nebius API key for the LLM
34
+ - `VECTARA_API_KEY` - Your Vectara API key for RAG
35
+ - Volume: `youtwo-volume` - For storing any persistent data
36
+
37
+ ## Project Structure
38
+
39
+ - `modal_app.py`: The main Modal deployment file
40
+ - `agent.py`: Core agent logic
41
+ - `rag.py`: Retrieval-augmented generation module
42
+ - `schemas.py`: Data schemas for the app
43
+
44
+ ## Deployment Instructions
45
+
46
+ ### Local Development
47
+
48
+ To run the app locally during development:
49
+
50
+ ```bash
51
+ modal serve modal_app.py
52
+ ```
53
+
54
+ This will provide you with a URL to access your app during development.
55
+
56
+ ### Production Deployment
57
+
58
+ To deploy the app to Modal for production:
59
+
60
+ ```bash
61
+ modal deploy modal_app.py
62
+ ```
63
+
64
+ After deployment, you'll receive a URL where your application is accessible.
65
+
66
+ ## Environment Variables
67
+
68
+ The application requires the following environment variables:
69
+
70
+ - `NEBIUS_API_KEY`: API key for accessing the Nebius LLM
71
+ - `VECTARA_API_KEY`: API key for Vectara RAG system
72
+
73
+ These should be set up as secrets in Modal.
74
+
75
+ ## Troubleshooting
76
+
77
+ - **Issue:** "WebRTC peer connection failed"
78
+ - **Solution:** Ensure your browser has permission to access your microphone and that you're using HTTPS or localhost.
79
+
80
+ - **Issue:** "Modal volume not accessible"
81
+ - **Solution:** Verify that you've created the `youtwo-volume` volume in Modal.
app.py CHANGED
@@ -1,165 +1,8 @@
1
- import gradio as gr
2
- from pathlib import Path
3
- from rag import is_allowed_filetype, upload_file_to_vectara, retrieve_chunks
4
- import logging
5
 
6
- # ---------------------------
7
- # Placeholder Backend Functions
8
- # ---------------------------
9
 
10
- def sync_lifelog_db() -> str:
11
- """
12
- Synchronizes local copy of lifelog (journal) database with the latest source.
13
- This function acts as a placeholder for database connection logic.
14
-
15
- Returns:
16
- str: Status message with sync timestamp.
17
- """
18
- import datetime
19
- return f"✅ Lifelog database synchronized successfully at {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
20
-
21
-
22
- def search_lifelogs(keyword: str, date_start: str, date_end: str) -> str:
23
- """
24
- Searches for lifelog entries containing the provided keyword within the specified date range.
25
-
26
- Args:
27
- keyword (str): Search term to match against entries.
28
- date_start (str): Start date (YYYY-MM-DD format).
29
- date_end (str): End date (inclusive, YYYY-MM-DD format).
30
-
31
- Returns:
32
- str: Simulated search result summary. Replace with actual search logic.
33
- """
34
- return f"🔍 Found 12 entries related to ‘{keyword}’ between {date_start} and {date_end}."
35
-
36
-
37
- def update_knowledge_graph_relations() -> str:
38
- """
39
- Updates the knowledge graph using the latest lifelog data.
40
- Simulates the creation of new triples (subject-predicate-object) from lifelog content.
41
-
42
- Returns:
43
- str: Update summary with number of relations added.
44
- """
45
- return "🧠 15 new triples have been added to the knowledge graph."
46
-
47
-
48
- def natural_language_handler(query: str) -> str:
49
- """
50
- Processes natural language inputs to determine which system function to execute.
51
- Designed to interface with NLP/LLM for future automation.
52
-
53
- Args:
54
- query (str): Free-text input from the user.
55
-
56
- Returns:
57
- str: Simulated or generated action and result.
58
- """
59
- chunks, response = retrieve_chunks(query, limit=5)
60
- return f"💬 Got {len(chunks)} chunks for your request: “{query}”. Response: {response}"
61
-
62
-
63
- def placeholder(feature_name: str = "unknown") -> str:
64
- """
65
- Placeholder for unimplemented features.
66
-
67
- Args:
68
- feature_name (str): Name of the feature to query.
69
-
70
- Returns:
71
- str: Placeholder response for future functions.
72
- """
73
- return f"{feature_name} functionality not available yet." # Replace with dynamic logic later
74
-
75
-
76
- # Gradio Behavior:
77
- # Textbox: As input component: Passes text value as a str into the function.
78
- # File: As input component: Passes the filepath to a temporary file object whose full path can be retrieved by file_obj.name
79
- # If we change the type to "binary", uploaded_file returns bytes.
80
- # NOTE: This means we can handle multiple files by tweaking this expected type.
81
- def handle_file_input(file_path: str | None, uploaded_file: gr.File | None):
82
- if not uploaded_file and not file_path:
83
- return "Please enter a file path or upload a file."
84
-
85
- if uploaded_file:
86
- filepath = Path(uploaded_file.name)
87
- else:
88
- filepath = Path(file_path.strip())
89
-
90
- if not filepath.exists():
91
- logging.error(f"Error: The specified file path does not exist: {filepath}")
92
- return "Error: The uploaded filepath does not exist."
93
-
94
- if not is_allowed_filetype(filepath.suffix):
95
- return f"Error: The uploaded filetype {filepath.suffix} is not supported."
96
-
97
- # Obtain the bytes
98
- with open(filepath, "rb") as file:
99
- file_contents = file.read()
100
-
101
-
102
- upload_result = upload_file_to_vectara(file_contents, filepath.name)
103
-
104
- return f"Uploaded document: {upload_result['id']}"
105
-
106
- # ---------------------------
107
- # Gradio UI (Blocks API)
108
- # ---------------------------
109
-
110
- with gr.Blocks(title="Knowledge Graph Agent Interface") as demo:
111
- gr.Markdown("## 🧠 Knowledge Graph Agent Interface\nBuilt with Gradio + MCP Support for LLM Tool Integration")
112
-
113
- with gr.Tab("🔄 Sync Lifelog DB"):
114
- gr.Markdown("Synchronize the lifelog database locally.")
115
- sync_btn = gr.Button("Sync Database")
116
- sync_out = gr.Textbox(lines=2, label="Sync Status")
117
- sync_btn.click(fn=sync_lifelog_db, outputs=sync_out)
118
-
119
- with gr.Tab("🔍 Search Lifelogs"):
120
- gr.Markdown("Search lifelog entries by keyword and time range.")
121
- keyword = gr.Textbox(label="Search Keyword")
122
- with gr.Row():
123
- start_date = gr.Textbox(label="Start Date (YYYY-MM-DD)")
124
- end_date = gr.Textbox(label="End Date (YYYY-MM-DD)")
125
- search_btn = gr.Button("Search Entries")
126
- search_out = gr.Textbox(label="Search Results")
127
- search_btn.click(fn=search_lifelogs, inputs=[keyword, start_date, end_date], outputs=search_out)
128
-
129
- with gr.Tab("🧠 Update Knowledge Graph"):
130
- gr.Markdown("Use lifelog data to update knowledge graph relations.")
131
- update_btn = gr.Button("Update Graph")
132
- update_out = gr.Textbox(label="Update Status")
133
- update_btn.click(fn=update_knowledge_graph_relations, outputs=update_out)
134
-
135
- with gr.Tab("🗣️ Natural Language Mode"):
136
- gr.Markdown("Input natural language requests for system actions.")
137
- with gr.Row():
138
- user_query = gr.Textbox(label="Type your query")
139
- query_btn = gr.Button("Process Request")
140
- query_out = gr.Textbox(label="System Response")
141
- query_btn.click(fn=natural_language_handler, inputs=user_query, outputs=query_out)
142
-
143
- with gr.Tab("⚙️ Future Features"):
144
- gr.Markdown("Placeholder area for upcoming functionalities")
145
- feature = gr.Textbox(label="Feature to Check")
146
- feature_btn = gr.Button("Check Feature Status")
147
- feature_out = gr.Textbox(label="Status")
148
- feature_btn.click(fn=placeholder, inputs=feature, outputs=feature_out)
149
-
150
- with gr.Tab("🗣️Input File"):
151
- gr.Markdown("Input file")
152
- with gr.Row():
153
- file_path_input = gr.Textbox(label="Enter File Path")
154
- file_upload_input = gr.File(label="Upload a File", type="filepath")
155
- submit_btn = gr.Button("Submit")
156
- output = gr.Textbox(label="Result")
157
- submit_btn.click(fn=handle_file_input, inputs=[file_path_input, file_upload_input], outputs=output)
158
-
159
- # ---------------------------
160
- # Launch as MCP Server
161
- # ---------------------------
162
  if __name__ == "__main__":
163
- from dotenv import load_dotenv
164
- load_dotenv()
165
- demo.launch()
 
1
+ # Run this gradio app with:
2
+ # python run_gradio_app.py
 
 
3
 
4
+ from src.yt_gradio.app import get_gradio_blocks
 
 
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  if __name__ == "__main__":
7
+ blocks = get_gradio_blocks()
8
+ blocks.launch()
 
chatbot-app.py DELETED
@@ -1,64 +0,0 @@
1
- # import gradio as gr
2
- # from huggingface_hub import InferenceClient
3
-
4
- # """
5
- # For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
6
- # """
7
- # client = InferenceClient("HuggingFaceH4/zephyr-7b-beta")
8
-
9
-
10
- # def respond(
11
- # message,
12
- # history: list[tuple[str, str]],
13
- # system_message,
14
- # max_tokens,
15
- # temperature,
16
- # top_p,
17
- # ):
18
- # messages = [{"role": "system", "content": system_message}]
19
-
20
- # for val in history:
21
- # if val[0]:
22
- # messages.append({"role": "user", "content": val[0]})
23
- # if val[1]:
24
- # messages.append({"role": "assistant", "content": val[1]})
25
-
26
- # messages.append({"role": "user", "content": message})
27
-
28
- # response = ""
29
-
30
- # for message in client.chat_completion(
31
- # messages,
32
- # max_tokens=max_tokens,
33
- # stream=True,
34
- # temperature=temperature,
35
- # top_p=top_p,
36
- # ):
37
- # token = message.choices[0].delta.content
38
-
39
- # response += token
40
- # yield response
41
-
42
-
43
- # """
44
- # For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
- # """
46
- # demo = gr.ChatInterface(
47
- # respond,
48
- # additional_inputs=[
49
- # gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
50
- # gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
51
- # gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
52
- # gr.Slider(
53
- # minimum=0.1,
54
- # maximum=1.0,
55
- # value=0.95,
56
- # step=0.05,
57
- # label="Top-p (nucleus sampling)",
58
- # ),
59
- # ],
60
- # )
61
-
62
-
63
- # if __name__ == "__main__":
64
- # demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,5 +1,14 @@
1
- gradio[mcp]==5.33.0
 
 
2
  requests
3
  python-dotenv
4
- smolagents
5
- fastrtc
 
 
 
 
 
 
 
 
1
+ gradio==5.33.0
2
+ modal==1.0.2
3
+ smolagents==1.17.0
4
  requests
5
  python-dotenv
6
+
7
+ # For knowledge graph
8
+ # langgraph
9
+ # langchain_core
10
+ # matplotlib
11
+ # networkx
12
+
13
+ # For vectara (Useful to get types)
14
+ # vectara
schemas.py DELETED
@@ -1,11 +0,0 @@
1
- from typing import TypedDict, Dict, Any
2
-
3
- class StorageUsage(TypedDict):
4
- bytes_used: int
5
- metadata_bytes_used: int
6
-
7
-
8
- class UploadResult(TypedDict):
9
- id: str
10
- metadata: Dict[str, Any]
11
- storage_usage: StorageUsage
 
 
 
 
 
 
 
 
 
 
 
 
yt_agent/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Agent module for YouTwo
3
+ """
yt_agent/agent.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from dotenv import load_dotenv
4
+ from smolagents import CodeAgent, InferenceClientModel
5
+
6
+ from src.yt_agent.tools import retrieve_tool, inspect_database_tool
7
+
8
+ # Load environment variables
9
+ load_dotenv()
10
+ # Initialize models
11
+ model = InferenceClientModel(provider="nebius", model="nebius/Qwen/Qwen3-30B-A3B", api_key=os.environ["NEBIUS_API_KEY"])
12
+ agent = CodeAgent(
13
+ tools=[
14
+ retrieve_tool,
15
+ inspect_database_tool,
16
+ ],
17
+ model=model,
18
+ max_steps=2,
19
+ verbosity_level=2,
20
+ description="Agent used to search documents.",
21
+ )
22
+
23
+ if __name__ == "__main__":
24
+ agent.run("What is 2+2?")
25
+ messages = agent.memory.get_full_steps()
26
+ print(messages)
yt_agent/prompts.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ AGENTIC_MODE_SYSTEM_PROMPT = """You are a general-purpose chatbot that answers user questions using information from uploaded documents.
2
+ When a question requires document-based information:
3
+ 1. Use the retrieval tool to fetch relevant document chunks
4
+ 2. Synthesize answers using these chunks
5
+ 3. Always cite your document sources
6
+
7
+ If information isn't in documents, use your general knowledge but state this limitation.
8
+ """
yt_agent/tools.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from smolagents import tool
2
+ from src.schemas import VectaraDocuments
3
+ from src.yt_rag.rag import fetch_documents_from_corpus, retrieve_chunks
4
+
5
+ @tool
6
+ def retrieve_tool(query: str, limit: int = 5, filter_by_id: str = None) -> dict[str, list[str] | str]:
7
+ """
8
+ Retrieve chunks by relevance to a query
9
+
10
+ Args:
11
+ query: The query to retrieve chunks for
12
+ limit: The maximum number of chunks to retrieve (default: 5)
13
+ filter_by_id: A document ID to filter by
14
+ Returns:
15
+ A list of chunks, and a grounded summary
16
+ """
17
+ chunks, vectara_summary = retrieve_chunks(query, limit, filter_by_id)
18
+ return {
19
+ "chunks": chunks,
20
+ "summary": vectara_summary
21
+ }
22
+
23
+ @tool
24
+ def inspect_database_tool() -> str:
25
+ """
26
+ Inspect the vector database
27
+
28
+ Returns:
29
+ A list of documents
30
+ """
31
+ results = fetch_documents_from_corpus(limit = 50)
32
+ documents = VectaraDocuments(documents = results["documents"])
33
+ id_list = [document["id"] for document in documents["documents"]]
34
+ final_string = "The following documents IDs are in the vector database:\n"
35
+ for i, id in enumerate(id_list):
36
+ final_string += f"{i+1}. {id}\n"
37
+ return final_string
yt_gradio/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """
2
+ Gradio UI module for YouTwo
3
+ """
yt_gradio/app.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pprint import pprint
2
+ import gradio as gr
3
+ from pathlib import Path
4
+ from src.yt_rag.rag import is_allowed_filetype, upload_file_to_vectara, retrieve_chunks
5
+ import logging
6
+ from src.yt_agent.agent import agent
7
+ # ---------------------------
8
+ # Placeholder Backend Functions
9
+ # ---------------------------
10
+
11
+
12
+ def update_knowledge_graph_relations() -> str:
13
+ """
14
+ Updates the knowledge graph using the latest lifelog data.
15
+ Simulates the creation of new triples (subject-predicate-object) from lifelog content.
16
+
17
+ Returns:
18
+ str: Update summary with number of relations added.
19
+ """
20
+ return "🧠 15 new triples have been added to the knowledge graph."
21
+
22
+
23
+ def natural_language_handler(query: str) -> str:
24
+ """
25
+ Processes natural language inputs to determine which system function to execute.
26
+ Designed to interface with NLP/LLM for future automation.
27
+
28
+ Args:
29
+ query (str): Free-text input from the user.
30
+
31
+ Returns:
32
+ str: Simulated or generated action and result.
33
+ """
34
+ chunks, response = retrieve_chunks(query, limit=5)
35
+ return f"💬 Got {len(chunks)} chunks for your request: “{query}”. Response: {response}"
36
+
37
+ def agent_chat(message: str, chat_history):
38
+ if not message.strip():
39
+ return chat_history, ""
40
+
41
+ # Append user message to history
42
+ chat_history.append({"role": "user", "content": message})
43
+
44
+ # Run your agent
45
+ response = agent.run(message)
46
+ if isinstance(response, dict):
47
+ parsed_response = response.get("output") or response.get("answer") or str(response)
48
+ else:
49
+ parsed_response = str(response)
50
+
51
+ # Append agent response to history
52
+ chat_history.append({"role": "assistant", "content": parsed_response})
53
+
54
+ return chat_history, ""
55
+
56
+ # Gradio Behavior:
57
+ # Textbox: As input component: Passes text value as a str into the function.
58
+ # File: As input component: Passes the filepath to a temporary file object whose full path can be retrieved by file_obj.name
59
+ # If we change the type to "binary", uploaded_file returns bytes.
60
+ # NOTE: This means we can handle multiple files by tweaking this expected type.
61
+ def handle_file_input(file_path: str | None, uploaded_file: gr.File | None):
62
+ if not uploaded_file and not file_path:
63
+ return "Please enter a file path or upload a file."
64
+
65
+ if uploaded_file:
66
+ filepath = Path(uploaded_file.name)
67
+ else:
68
+ filepath = Path(file_path.strip())
69
+
70
+ if not filepath.exists():
71
+ logging.error(f"Error: The specified file path does not exist: {filepath}")
72
+ return "Error: The uploaded filepath does not exist."
73
+
74
+ if not is_allowed_filetype(filepath.suffix):
75
+ return f"Error: The uploaded filetype {filepath.suffix} is not supported."
76
+
77
+ # Obtain the bytes
78
+ with open(filepath, "rb") as file:
79
+ file_contents = file.read()
80
+
81
+
82
+ upload_result = upload_file_to_vectara(file_contents, filepath.name)
83
+
84
+ return f"Uploaded document: {upload_result['id']}"
85
+
86
+ # ---------------------------
87
+ # Gradio UI (Blocks API)
88
+ # ---------------------------
89
+
90
+ def get_gradio_blocks():
91
+ with gr.Blocks(title="Knowledge Graph Agent Interface") as demo:
92
+ gr.Markdown("## 🧠 Knowledge Graph Agent Interface\nBuilt with Gradio + MCP Support for LLM Tool Integration")
93
+
94
+ with gr.Tab("🗣️ Natural Language Mode"):
95
+ gr.Markdown("Input natural language requests for system actions.")
96
+ with gr.Row():
97
+ user_query = gr.Textbox(label="Type your query")
98
+ query_btn = gr.Button("Process Request")
99
+ query_out = gr.Textbox(label="System Response")
100
+ query_btn.click(fn=natural_language_handler, inputs=user_query, outputs=query_out)
101
+
102
+ with gr.Tab("⚙️ Agentic Chat"):
103
+ gr.Markdown("Agentic Question and Answer1")
104
+ chatbot = gr.Chatbot(label="KG Agent", height=500, show_label=True, container=True, type="messages",
105
+ bubble_full_width=False,
106
+ value=[
107
+ {"role": "assistant", "content": "👋 Hello! I'm the KG Agent, your intelligent assistant for serving KG. How can I help you today?"}
108
+ ])
109
+ user_input = gr.Textbox(placeholder="Type your question...", label="Message", lines=2, scale=4, show_label=False, value="Inspect the vector database. Tell me how many documents are in the database.")
110
+ #clear_button = gr.Button("🗑️ Clear Chat", size="sm")
111
+ send_btn = gr.Button("Send", variant="primary", scale=1)
112
+
113
+ # Wire up the button (and hitting Enter) to call `agent_chat`
114
+ send_btn.click(
115
+ fn=agent_chat,
116
+ inputs=[user_input, chatbot],
117
+ outputs=[chatbot, user_input],
118
+ show_progress=True
119
+ )
120
+ user_input.submit(
121
+ fn=agent_chat,
122
+ inputs=[user_input, chatbot],
123
+ outputs=[chatbot, user_input],
124
+ #outputs=[self.chatbot, self.message_input, self.context_display, self.suggestions_display],
125
+ )
126
+
127
+ with gr.Tab("🗣️Input File"):
128
+ gr.Markdown("Input file")
129
+ with gr.Row():
130
+ file_path_input = gr.Textbox(label="Enter File Path")
131
+ file_upload_input = gr.File(label="Upload a File", type="filepath")
132
+ submit_btn = gr.Button("Submit")
133
+ output = gr.Textbox(label="Result")
134
+ submit_btn.click(fn=handle_file_input, inputs=[file_path_input, file_upload_input], outputs=output)
135
+
136
+ return demo
137
+
138
+ # ---------------------------
139
+ # Launch as MCP Server
140
+ # ---------------------------
141
+ if __name__ == "__main__":
142
+ from dotenv import load_dotenv
143
+ load_dotenv()
144
+ demo = get_gradio_blocks()
145
+ demo.launch(mcp_server=True)
yt_rag/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """
2
+ RAG (Retrieval Augmented Generation) module for YouTwo
3
+ """
4
+
5
+ # from .rag import retrieve_chunks, is_allowed_filetype, upload_file_to_vectara
rag.py → yt_rag/rag.py RENAMED
@@ -4,7 +4,8 @@ import os
4
  from pathlib import Path
5
  import requests
6
  from pprint import pprint
7
- from schemas import UploadResult
 
8
 
9
  logger = logging.getLogger(__name__)
10
  logger.setLevel(logging.INFO)
@@ -34,6 +35,7 @@ def load_environment_variables():
34
  raise IndexingError("Vectara API key not set. Please set the VECTARA_API_KEY environment variable.")
35
 
36
 
 
37
  def is_allowed_filetype(suffix: str):
38
  # Commonmark / Markdown (md extension).
39
  # PDF/A (pdf).
@@ -45,7 +47,7 @@ def is_allowed_filetype(suffix: str):
45
  # LXML files (.lxml).
46
  # RTF files (.rtf).
47
  # ePUB files (.epub).
48
- return suffix in [".pdf", ".odt", ".doc", ".docx", ".ppt", ".pptx", ".txt", ".html", ".lxml", ".rtf", ".epub"]
49
 
50
  def save_response_to_file(response_json: dict, filename: str):
51
  """
@@ -135,7 +137,7 @@ def process_upload_response(response_json: dict) -> UploadResult:
135
  storage_usage=response_json["storage_usage"]
136
  )
137
  # See https://docs.vectara.com/docs/rest-api/query-corpus
138
- def retrieve_chunks(query: str, limit: int = 10) -> tuple[list[str], str]:
139
  """
140
  Retrieves relevant chunks and a generated summary from the Vectara corpus based on the query.
141
 
@@ -156,18 +158,19 @@ def retrieve_chunks(query: str, limit: int = 10) -> tuple[list[str], str]:
156
  "x-api-key": api_key,
157
  "Content-Type": "application/json"
158
  }
 
 
 
 
 
 
 
 
 
 
159
  payload = {
160
  "query": query,
161
- "search": {
162
- "limit": limit, # Number of search results to retrieve
163
- # "reranker": {
164
- # "type": "customer_reranker",
165
- # "reranker_name": "Rerank_Multilingual_v1",
166
- # "limit": 0,
167
- # "cutoff": 0,
168
- # "include_context": True
169
- # }
170
- },
171
  "generation": {
172
  "generation_preset_name": "mockingbird-2.0", # Using Mockingbird for RAG
173
  "max_used_search_results": 5,
@@ -212,6 +215,128 @@ def retrieve_chunks(query: str, limit: int = 10) -> tuple[list[str], str]:
212
  except Exception as e:
213
  raise VectaraAPIError(f"An unexpected error occurred during Vectara query: {e}") from e
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
  # This is still a placeholder
217
  def generate_llm_response(chat_state: list[dict], retrieved_chunks: list[str], summary: str) -> str:
@@ -250,6 +375,7 @@ def test_file_upload():
250
  except Exception as e:
251
  raise IndexingError(f"Error occurred while uploading PDF: {e}")
252
 
 
253
  if __name__ == "__main__":
254
  from dotenv import load_dotenv
255
  load_dotenv()
 
4
  from pathlib import Path
5
  import requests
6
  from pprint import pprint
7
+ from src.schemas import UploadResult
8
+
9
 
10
  logger = logging.getLogger(__name__)
11
  logger.setLevel(logging.INFO)
 
35
  raise IndexingError("Vectara API key not set. Please set the VECTARA_API_KEY environment variable.")
36
 
37
 
38
+
39
  def is_allowed_filetype(suffix: str):
40
  # Commonmark / Markdown (md extension).
41
  # PDF/A (pdf).
 
47
  # LXML files (.lxml).
48
  # RTF files (.rtf).
49
  # ePUB files (.epub).
50
+ return suffix in [".md", ".pdf", ".odt", ".doc", ".docx", ".ppt", ".pptx", ".txt", ".html", ".lxml", ".rtf", ".epub"]
51
 
52
  def save_response_to_file(response_json: dict, filename: str):
53
  """
 
137
  storage_usage=response_json["storage_usage"]
138
  )
139
  # See https://docs.vectara.com/docs/rest-api/query-corpus
140
+ def retrieve_chunks(query: str, limit: int = 10, filter_by_id: str = None) -> tuple[list[str], str]:
141
  """
142
  Retrieves relevant chunks and a generated summary from the Vectara corpus based on the query.
143
 
 
158
  "x-api-key": api_key,
159
  "Content-Type": "application/json"
160
  }
161
+ metadata_filter = f"doc.id='{filter_by_id}'" if filter_by_id else None
162
+ if metadata_filter:
163
+ search = {
164
+ "metadata_filter": metadata_filter,
165
+ "limit": limit,
166
+ }
167
+ else:
168
+ search = {
169
+ "limit": limit,
170
+ }
171
  payload = {
172
  "query": query,
173
+ "search": search,
 
 
 
 
 
 
 
 
 
174
  "generation": {
175
  "generation_preset_name": "mockingbird-2.0", # Using Mockingbird for RAG
176
  "max_used_search_results": 5,
 
215
  except Exception as e:
216
  raise VectaraAPIError(f"An unexpected error occurred during Vectara query: {e}") from e
217
 
218
+ def fetch_documents_from_corpus(limit: int = 10, metadata_filter: str = None, page_key: str = None) -> dict:
219
+ """
220
+ Fetches documents from a specific Vectara corpus.
221
+
222
+ Args:
223
+ limit (int, optional): Maximum number of documents to return. Must be between 1 and 100. Defaults to 10.
224
+ metadata_filter (str, optional): Filter documents by metadata. Uses expression similar to query metadata filter.
225
+ page_key (str, optional): Key used to retrieve the next page of documents after the limit has been reached.
226
+ request_timeout (int, optional): Time in seconds the API will attempt to complete the request before timing out.
227
+ request_timeout_millis (int, optional): Time in milliseconds the API will attempt to complete the request.
228
+
229
+ Returns:
230
+ dict: The response from the Vectara API containing the requested documents.
231
+
232
+ Raises:
233
+ VectaraAPIError: If there's an error with the Vectara API request.
234
+ """
235
+ import os
236
+ import requests
237
+ CORPUS_KEY = "YouTwo"
238
+ request_timeout = 20
239
+ request_timeout_millis = 60000
240
+
241
+
242
+ # Validate inputs
243
+ if limit is not None and (limit < 1 or limit > 100):
244
+ raise ValueError("Limit must be between 1 and 100")
245
+
246
+ if len(CORPUS_KEY) > 50 or not all(c.isalnum() or c in ['_', '=', '-'] for c in CORPUS_KEY):
247
+ raise ValueError("corpus_key must be <= 50 characters and match regex [a-zA-Z0-9_\\=\\-]+$")
248
+
249
+ # Prepare request
250
+ vectara_api_key = os.getenv("VECTARA_API_KEY")
251
+
252
+ if not vectara_api_key:
253
+ raise VectaraAPIError("Vectara API key not found in environment variables")
254
+
255
+ url = f"https://api.vectara.io/v2/corpora/{CORPUS_KEY}/documents"
256
+
257
+ headers = {
258
+ "Accept": "application/json",
259
+ "x-api-key": vectara_api_key
260
+ }
261
+
262
+ payload = {}
263
+
264
+ # Build query params
265
+ params = {}
266
+ if limit is not None:
267
+ params["limit"] = limit
268
+ if metadata_filter is not None:
269
+ params["metadata_filter"] = metadata_filter
270
+ if page_key is not None:
271
+ params["page_key"] = page_key
272
+ try:
273
+ response = requests.get(url, headers=headers, params=params)
274
+ response.raise_for_status()
275
+ return response.json()
276
+ except requests.exceptions.RequestException as e:
277
+ raise VectaraAPIError(f"Error fetching documents from Vectara corpus: {e}") from e
278
+ except Exception as e:
279
+ raise VectaraAPIError(f"An unexpected error occurred while fetching documents: {e}") from e
280
+
281
+ def fetch_document_by_id(document_id: str) -> dict:
282
+ """
283
+ Retrieves the content and metadata of a specific document by its ID.
284
+
285
+ Args:
286
+ document_id (str): The document ID to retrieve. Must be percent encoded.
287
+
288
+ Returns:
289
+ dict: The document data including content and metadata.
290
+
291
+ Raises:
292
+ VectaraAPIError: If there's an error with the Vectara API request.
293
+ """
294
+ import os
295
+ import requests
296
+ from urllib.parse import quote
297
+
298
+ CORPUS_KEY = "YouTwo"
299
+ request_timeout = 20
300
+ request_timeout_millis = 60000
301
+
302
+ # Validate corpus key
303
+ if len(CORPUS_KEY) > 50 or not all(c.isalnum() or c in ['_', '=', '-'] for c in CORPUS_KEY):
304
+ raise ValueError("corpus_key must be <= 50 characters and match regex [a-zA-Z0-9_\\=\\-]+$")
305
+
306
+ # Prepare request
307
+ vectara_api_key = os.getenv("VECTARA_API_KEY")
308
+
309
+ if not vectara_api_key:
310
+ raise VectaraAPIError("Vectara API key not found in environment variables")
311
+
312
+ # Ensure document_id is percent encoded
313
+ encoded_document_id = quote(document_id)
314
+
315
+ url = f"https://api.vectara.io/v2/corpora/{CORPUS_KEY}/documents/{encoded_document_id}"
316
+
317
+ headers = {
318
+ "Accept": "application/json",
319
+ "x-api-key": vectara_api_key
320
+ }
321
+
322
+ payload = {}
323
+
324
+ # Set timeout parameters if needed
325
+ params = {}
326
+ if request_timeout is not None:
327
+ headers["Request-Timeout"] = str(request_timeout)
328
+ if request_timeout_millis is not None:
329
+ headers["Request-Timeout-Millis"] = str(request_timeout_millis)
330
+
331
+ try:
332
+ response = requests.get(url, headers=headers, params=params, data=payload)
333
+ response.raise_for_status()
334
+ return response.json()
335
+ except requests.exceptions.RequestException as e:
336
+ raise VectaraAPIError(f"Error fetching document from Vectara: {e}") from e
337
+ except Exception as e:
338
+ raise VectaraAPIError(f"An unexpected error occurred while fetching document: {e}") from e
339
+
340
 
341
  # This is still a placeholder
342
  def generate_llm_response(chat_state: list[dict], retrieved_chunks: list[str], summary: str) -> str:
 
375
  except Exception as e:
376
  raise IndexingError(f"Error occurred while uploading PDF: {e}")
377
 
378
+
379
  if __name__ == "__main__":
380
  from dotenv import load_dotenv
381
  load_dotenv()