Spaces:
Sleeping
Sleeping
updated UI and added CSS
Browse files- .gitignore +2 -1
- LICENSE +21 -0
- README.md +50 -3
- agent.py +35 -4
- agent_langgraph.py +0 -399
- app.py +275 -156
- static/custom.css +478 -0
- tools.py +5 -3
.gitignore
CHANGED
@@ -15,4 +15,5 @@ __pycache__/*
|
|
15 |
*.pyo
|
16 |
*.pyd
|
17 |
|
18 |
-
TEMPP/*
|
|
|
|
15 |
*.pyo
|
16 |
*.pyd
|
17 |
|
18 |
+
TEMPP/*
|
19 |
+
test.txt
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2025 Vividh Mahajan
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
colorFrom: yellow
|
5 |
colorTo: red
|
6 |
sdk: gradio
|
@@ -11,4 +11,51 @@ hf_oauth: true
|
|
11 |
hf_oauth_expiration_minutes: 480
|
12 |
---
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: ScholarAI
|
3 |
+
emoji: 🎓
|
4 |
colorFrom: yellow
|
5 |
colorTo: red
|
6 |
sdk: gradio
|
|
|
11 |
hf_oauth_expiration_minutes: 480
|
12 |
---
|
13 |
|
14 |
+
# ScholarAI 🎓
|
15 |
+
|
16 |
+
[](https://huggingface.co/spaces/Lasdw/ScholarAI)
|
17 |
+
[](https://opensource.org/licenses/MIT)
|
18 |
+
[](https://huggingface.co/spaces/Lasdw/ScholarAI)
|
19 |
+
[](https://www.python.org/downloads/)
|
20 |
+
[](https://gradio.app/)
|
21 |
+
[](https://github.com/Lasdw/ScholarAI)
|
22 |
+
|
23 |
+
An AI-powered research assistant that helps you find answers by searching the web, analyzing images, processing audio, and more.
|
24 |
+
|
25 |
+
## Features
|
26 |
+
|
27 |
+
- Web search and Wikipedia integration
|
28 |
+
- Image analysis
|
29 |
+
- Audio processing
|
30 |
+
- Code analysis
|
31 |
+
- Data file processing
|
32 |
+
|
33 |
+
## Requirements
|
34 |
+
|
35 |
+
```bash
|
36 |
+
pip install -r requirements.txt
|
37 |
+
```
|
38 |
+
|
39 |
+
Key dependencies:
|
40 |
+
|
41 |
+
- gradio: Web interface
|
42 |
+
- langchain & langgraph: AI agent framework
|
43 |
+
- openai: Language model integration
|
44 |
+
- beautifulsoup4 & html2text: Web scraping
|
45 |
+
- pytube & youtube-transcript-api: Video processing
|
46 |
+
- whisper: Audio transcription
|
47 |
+
- pandas & openpyxl: Data processing
|
48 |
+
- Pillow: Image processing
|
49 |
+
- PyPDF2 & pymupdf: PDF handling
|
50 |
+
|
51 |
+
## License
|
52 |
+
|
53 |
+
[](https://opensource.org/licenses/MIT)
|
54 |
+
|
55 |
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
56 |
+
|
57 |
+
## Author
|
58 |
+
|
59 |
+
Created by [Vividh Mahajan](https://huggingface.co/Lasdw)
|
60 |
+
|
61 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
agent.py
CHANGED
@@ -57,7 +57,7 @@ load_dotenv()
|
|
57 |
#webpage_scrape: Scrape content from a specific webpage URL when Tavily Search and Wikipedia Search do not return a result. Provide a valid URL to extract information from a particular web page.
|
58 |
#Give preference to using Tavily Search and Wikipedia Search before using web_search or webpage_scrape. When Web_search does not return a result, use Tavily Search.
|
59 |
|
60 |
-
SYSTEM_PROMPT = """ You are a genuis deep reseach assistant called
|
61 |
For simple questions, you can use your internal knowledge and answer directly. If you do not understand the question, ask for clarification after trying to answer the question yourself.
|
62 |
|
63 |
The way you use the tools is by specifying a json blob. These are the only tools you can use:
|
@@ -1320,16 +1320,47 @@ def create_agent_graph() -> StateGraph:
|
|
1320 |
return builder.compile()
|
1321 |
|
1322 |
# Main agent class that integrates with your existing app.py
|
1323 |
-
class
|
1324 |
-
def __init__(self, max_iterations=35, apify_api_token=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1325 |
self.graph = create_agent_graph()
|
1326 |
self.tools = tools_config
|
1327 |
self.max_iterations = max_iterations # Maximum iterations for the graph
|
1328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1329 |
# Set Apify API token if provided
|
1330 |
if apify_api_token:
|
1331 |
os.environ["APIFY_API_TOKEN"] = apify_api_token
|
1332 |
print("Apify API token set successfully")
|
|
|
|
|
1333 |
|
1334 |
def __call__(self, question: str, attachments: dict = None) -> str:
|
1335 |
"""
|
@@ -1392,7 +1423,7 @@ class TurboNerd:
|
|
1392 |
|
1393 |
# Example usage:
|
1394 |
if __name__ == "__main__":
|
1395 |
-
agent =
|
1396 |
response = agent("""The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places. TEMPP\excel.xlsx """)
|
1397 |
print("\nFinal Response:")
|
1398 |
print(response)
|
|
|
57 |
#webpage_scrape: Scrape content from a specific webpage URL when Tavily Search and Wikipedia Search do not return a result. Provide a valid URL to extract information from a particular web page.
|
58 |
#Give preference to using Tavily Search and Wikipedia Search before using web_search or webpage_scrape. When Web_search does not return a result, use Tavily Search.
|
59 |
|
60 |
+
SYSTEM_PROMPT = """ You are a genuis deep reseach assistant called ScholarAI, made by Vividh Mahajan. Answer the following questions as best you can. If it is a basic question, answer it using your internal knowledge. If it is a complex question that requires facts, use the tools to answer it DO NOT rely on your internal knowledge unless the tools fail to provide a result:
|
61 |
For simple questions, you can use your internal knowledge and answer directly. If you do not understand the question, ask for clarification after trying to answer the question yourself.
|
62 |
|
63 |
The way you use the tools is by specifying a json blob. These are the only tools you can use:
|
|
|
1320 |
return builder.compile()
|
1321 |
|
1322 |
# Main agent class that integrates with your existing app.py
|
1323 |
+
class ScholarAI:
|
1324 |
+
def __init__(self, max_iterations=35, temperature=0.1, max_tokens=2000, model="gpt-4o-mini", apify_api_token=None):
|
1325 |
+
# Check for OpenAI API key
|
1326 |
+
if not os.getenv("OPENAI_API_KEY"):
|
1327 |
+
raise ValueError("OpenAI API key not found. Please set the OPENAI_API_KEY environment variable.")
|
1328 |
+
|
1329 |
+
try:
|
1330 |
+
# Test the API key with a simple request
|
1331 |
+
test_llm = ChatOpenAI(
|
1332 |
+
model=model,
|
1333 |
+
temperature=temperature,
|
1334 |
+
max_tokens=max_tokens
|
1335 |
+
)
|
1336 |
+
test_llm.invoke("test") # This will fail if API key is invalid
|
1337 |
+
except Exception as e:
|
1338 |
+
error_msg = str(e).lower()
|
1339 |
+
if "invalid_api_key" in error_msg or "incorrect_api_key" in error_msg:
|
1340 |
+
raise ValueError("Invalid OpenAI API key. Please check your API key and try again.")
|
1341 |
+
elif "rate_limit" in error_msg or "quota" in error_msg:
|
1342 |
+
raise ValueError("OpenAI API rate limit exceeded or quota reached. Please try again later.")
|
1343 |
+
else:
|
1344 |
+
raise ValueError(f"Error initializing OpenAI client: {str(e)}")
|
1345 |
+
|
1346 |
self.graph = create_agent_graph()
|
1347 |
self.tools = tools_config
|
1348 |
self.max_iterations = max_iterations # Maximum iterations for the graph
|
1349 |
|
1350 |
+
# Update the global llm instance with the specified parameters
|
1351 |
+
global llm
|
1352 |
+
llm = ChatOpenAI(
|
1353 |
+
model=model,
|
1354 |
+
temperature=temperature,
|
1355 |
+
max_tokens=max_tokens
|
1356 |
+
)
|
1357 |
+
|
1358 |
# Set Apify API token if provided
|
1359 |
if apify_api_token:
|
1360 |
os.environ["APIFY_API_TOKEN"] = apify_api_token
|
1361 |
print("Apify API token set successfully")
|
1362 |
+
|
1363 |
+
print(f"ScholarAI initialized with model={model}, temperature={temperature}, max_tokens={max_tokens}")
|
1364 |
|
1365 |
def __call__(self, question: str, attachments: dict = None) -> str:
|
1366 |
"""
|
|
|
1423 |
|
1424 |
# Example usage:
|
1425 |
if __name__ == "__main__":
|
1426 |
+
agent = ScholarAI(max_iterations=25)
|
1427 |
response = agent("""The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places. TEMPP\excel.xlsx """)
|
1428 |
print("\nFinal Response:")
|
1429 |
print(response)
|
agent_langgraph.py
DELETED
@@ -1,399 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
from typing import TypedDict, Annotated
|
3 |
-
from langgraph.graph.message import add_messages
|
4 |
-
from langchain_core.messages import AnyMessage, HumanMessage, AIMessage, SystemMessage
|
5 |
-
from langgraph.prebuilt import ToolNode
|
6 |
-
from langchain.tools import Tool
|
7 |
-
from langgraph.graph import START, END, StateGraph
|
8 |
-
from langgraph.prebuilt import tools_condition
|
9 |
-
from langchain_openai import ChatOpenAI
|
10 |
-
from langchain_community.tools import DuckDuckGoSearchRun
|
11 |
-
import getpass
|
12 |
-
import subprocess
|
13 |
-
import tempfile
|
14 |
-
import time
|
15 |
-
import random
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
def run_python_code(code: str):
|
20 |
-
"""Execute Python code in a temporary file and return the output."""
|
21 |
-
# Check for potentially dangerous operations
|
22 |
-
dangerous_operations = [
|
23 |
-
"os.system", "os.popen", "os.unlink", "os.remove",
|
24 |
-
"subprocess.run", "subprocess.call", "subprocess.Popen",
|
25 |
-
"shutil.rmtree", "shutil.move", "shutil.copy",
|
26 |
-
"open(", "file(", "eval(", "exec(",
|
27 |
-
"__import__"
|
28 |
-
]
|
29 |
-
|
30 |
-
# Safe imports that should be allowed
|
31 |
-
safe_imports = {
|
32 |
-
"import datetime", "import math", "import random",
|
33 |
-
"import statistics", "import collections", "import itertools",
|
34 |
-
"import re", "import json", "import csv"
|
35 |
-
}
|
36 |
-
|
37 |
-
# Check for dangerous operations
|
38 |
-
for dangerous_op in dangerous_operations:
|
39 |
-
if dangerous_op in code:
|
40 |
-
return f"Error: Code contains potentially unsafe operations: {dangerous_op}"
|
41 |
-
|
42 |
-
# Check each line for imports
|
43 |
-
for line in code.splitlines():
|
44 |
-
line = line.strip()
|
45 |
-
if line.startswith("import ") or line.startswith("from "):
|
46 |
-
# Skip if it's in our safe list
|
47 |
-
if any(line.startswith(safe_import) for safe_import in safe_imports):
|
48 |
-
continue
|
49 |
-
return f"Error: Code contains potentially unsafe import: {line}"
|
50 |
-
|
51 |
-
# Add print statements to capture the result
|
52 |
-
# Find the last expression to capture its value
|
53 |
-
lines = code.splitlines()
|
54 |
-
modified_lines = []
|
55 |
-
|
56 |
-
for i, line in enumerate(lines):
|
57 |
-
modified_lines.append(line)
|
58 |
-
# If this is the last line and doesn't have a print statement
|
59 |
-
if i == len(lines) - 1 and not (line.strip().startswith("print(") or line.strip() == ""):
|
60 |
-
# Add a print statement for the last expression
|
61 |
-
if not line.strip().endswith(":"): # Not a control structure
|
62 |
-
modified_lines.append(f"print('Result:', {line.strip()})")
|
63 |
-
|
64 |
-
modified_code = "\n".join(modified_lines)
|
65 |
-
|
66 |
-
try:
|
67 |
-
# Create a temporary file
|
68 |
-
with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as temp:
|
69 |
-
temp_path = temp.name
|
70 |
-
# Write the code to the file
|
71 |
-
temp.write(modified_code.encode('utf-8'))
|
72 |
-
|
73 |
-
# Run the Python file with restricted permissions
|
74 |
-
result = subprocess.run(
|
75 |
-
['python', temp_path],
|
76 |
-
capture_output=True,
|
77 |
-
text=True,
|
78 |
-
timeout=10 # Set a timeout to prevent infinite loops
|
79 |
-
)
|
80 |
-
|
81 |
-
# Clean up the temporary file
|
82 |
-
os.unlink(temp_path)
|
83 |
-
|
84 |
-
# Return the output or error
|
85 |
-
if result.returncode == 0:
|
86 |
-
output = result.stdout.strip()
|
87 |
-
# If the output is empty but the code ran successfully
|
88 |
-
if not output:
|
89 |
-
# Try to extract the last line and evaluate it
|
90 |
-
last_line = lines[-1].strip()
|
91 |
-
if not last_line.startswith("print") and not last_line.endswith(":"):
|
92 |
-
return f"Code executed successfully. The result of the last expression '{last_line}' should be its value."
|
93 |
-
else:
|
94 |
-
return "Code executed successfully with no output."
|
95 |
-
return output
|
96 |
-
else:
|
97 |
-
return f"Error executing code: {result.stderr}"
|
98 |
-
except subprocess.TimeoutExpired:
|
99 |
-
# Clean up if timeout
|
100 |
-
os.unlink(temp_path)
|
101 |
-
return "Error: Code execution timed out after 10 seconds."
|
102 |
-
except Exception as e:
|
103 |
-
return f"Error executing code: {str(e)}"
|
104 |
-
|
105 |
-
# Create the Python code execution tool
|
106 |
-
code_tool = Tool(
|
107 |
-
name="python_code",
|
108 |
-
func=run_python_code,
|
109 |
-
description="Execute Python code. Provide the complete Python code as a string. The code will be executed and the output will be returned. Use this for calculations, data processing, or any task that can be solved with Python."
|
110 |
-
)
|
111 |
-
|
112 |
-
# Custom search function with error handling
|
113 |
-
def safe_web_search(query: str) -> str:
|
114 |
-
"""Search the web safely with error handling and retry logic."""
|
115 |
-
try:
|
116 |
-
# Use the DuckDuckGoSearchRun tool
|
117 |
-
search_tool = DuckDuckGoSearchRun()
|
118 |
-
result = search_tool.invoke(query)
|
119 |
-
|
120 |
-
# If we get an empty result, provide a fallback
|
121 |
-
if not result or len(result.strip()) < 10:
|
122 |
-
return f"Unable to find specific information about '{query}'. Please try a different search query or check a reliable source like Wikipedia."
|
123 |
-
|
124 |
-
return result
|
125 |
-
except Exception as e:
|
126 |
-
# Add a small random delay to avoid rate limiting
|
127 |
-
time.sleep(random.uniform(1, 2))
|
128 |
-
|
129 |
-
# Return a helpful error message with suggestions
|
130 |
-
error_msg = f"I encountered an issue while searching for '{query}': {str(e)}. "
|
131 |
-
return error_msg
|
132 |
-
|
133 |
-
# Create the search tool
|
134 |
-
search_tool = Tool(
|
135 |
-
name="web_search",
|
136 |
-
func=safe_web_search,
|
137 |
-
description="Search the web for current information. Provide a specific search query."
|
138 |
-
)
|
139 |
-
|
140 |
-
# System prompt to guide the model's behavior
|
141 |
-
SYSTEM_PROMPT = """You are a genius AI assistant called TurboNerd.
|
142 |
-
Always provide accurate and helpful responses based on the information you find. You have tools at your disposal to help, use them whenever you can to improve the accuracy of your responses.
|
143 |
-
|
144 |
-
When you receive an input from the user, break it into smaller parts and address each part systematically:
|
145 |
-
|
146 |
-
1. For information retrieval (like finding current data, statistics, etc.), use the web_search tool.
|
147 |
-
- If the search fails, don't repeatedly attempt identical searches. Provide the best information you have and be honest about limitations.
|
148 |
-
|
149 |
-
2. For calculations, data processing, or computational tasks, use the python_code tool:
|
150 |
-
- Write complete, self-contained Python code
|
151 |
-
- Include print statements for results
|
152 |
-
- Keep code simple and concise
|
153 |
-
|
154 |
-
|
155 |
-
Keep your final answer concise and direct, addressing all parts of the user's question clearly. DO NOT include any other text in your response, just the answer.
|
156 |
-
"""
|
157 |
-
#Your response will be evaluated for accuracy and completeness. After you provide an answer, an evaluator will check your work and may ask you to improve it. The evaluation process has a maximum of 3 attempts.
|
158 |
-
|
159 |
-
# Generate the chat interface, including the tools
|
160 |
-
llm = ChatOpenAI(
|
161 |
-
model="gpt-4o-mini",
|
162 |
-
temperature=0
|
163 |
-
)
|
164 |
-
|
165 |
-
chat = llm
|
166 |
-
tools = [search_tool, code_tool]
|
167 |
-
chat_with_tools = chat.bind_tools(tools)
|
168 |
-
|
169 |
-
# Generate the AgentState and Agent graph
|
170 |
-
class AgentState(TypedDict):
|
171 |
-
messages: Annotated[list[AnyMessage], add_messages]
|
172 |
-
|
173 |
-
def assistant(state: AgentState):
|
174 |
-
# Add system message if it's the first message
|
175 |
-
print("Assistant Called...\n\n")
|
176 |
-
print(f"Assistant state keys: {state.keys()}")
|
177 |
-
print(f"Assistant message count: {len(state['messages'])}")
|
178 |
-
|
179 |
-
if len(state["messages"]) == 1 and isinstance(state["messages"][0], HumanMessage):
|
180 |
-
messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
|
181 |
-
else:
|
182 |
-
messages = state["messages"]
|
183 |
-
|
184 |
-
response = chat_with_tools.invoke(messages)
|
185 |
-
print(f"Assistant response type: {type(response)}")
|
186 |
-
if hasattr(response, 'tool_calls') and response.tool_calls:
|
187 |
-
print(f"Tool calls detected: {len(response.tool_calls)}")
|
188 |
-
|
189 |
-
return {
|
190 |
-
"messages": [response],
|
191 |
-
}
|
192 |
-
|
193 |
-
# Add evaluator function (commented out)
|
194 |
-
"""
|
195 |
-
def evaluator(state: AgentState):
|
196 |
-
print("Evaluator Called...\n\n")
|
197 |
-
print(f"Evaluator state keys: {state.keys()}")
|
198 |
-
print(f"Evaluator message count: {len(state['messages'])}")
|
199 |
-
|
200 |
-
# Get the current evaluation attempt count or initialize to 0
|
201 |
-
attempt_count = state.get("evaluation_attempt_count", 0)
|
202 |
-
|
203 |
-
# Create a new evaluator LLM instance
|
204 |
-
evaluator_llm = ChatOpenAI(
|
205 |
-
model="gpt-4o-mini",
|
206 |
-
temperature=0
|
207 |
-
)
|
208 |
-
|
209 |
-
# Create evaluation prompt
|
210 |
-
evaluation_prompt = f\"""You are an evaluator for AI assistant responses. Your job is to:
|
211 |
-
|
212 |
-
1. Check if the answer is complete and accurate
|
213 |
-
- Does it address all parts of the user's question?
|
214 |
-
- Is the information factually correct to the best of your knowledge?
|
215 |
-
|
216 |
-
2. Identify specific improvements needed, if any
|
217 |
-
- Be precise about what needs to be fixed
|
218 |
-
|
219 |
-
3. Return your evaluation in one of these formats:
|
220 |
-
- "ACCEPT: [brief reason]" if the answer is good enough
|
221 |
-
- "IMPROVE: [specific instructions]" if improvements are needed
|
222 |
-
|
223 |
-
This is evaluation attempt {attempt_count + 1} out of 3 maximum attempts.
|
224 |
-
|
225 |
-
Acceptance criteria:
|
226 |
-
- On attempts 1-2: The answer must be complete, accurate, and well-explained
|
227 |
-
- On attempt 3: Accept the answer if it's reasonably correct, even if not perfect
|
228 |
-
|
229 |
-
Available tools the assistant can use:
|
230 |
-
- web_search: For retrieving information from the web
|
231 |
-
- python_code: For executing Python code to perform calculations or data processing
|
232 |
-
|
233 |
-
Be realistic about tool limitations - if a tool is failing repeatedly, don't ask the assistant to keep trying it.
|
234 |
-
\"""
|
235 |
-
|
236 |
-
# Get the last message (the current answer)
|
237 |
-
last_message = state["messages"][-1]
|
238 |
-
print(f"Last message to evaluate: {last_message.content}")
|
239 |
-
|
240 |
-
# Create evaluation message
|
241 |
-
evaluation_message = [
|
242 |
-
SystemMessage(content=evaluation_prompt),
|
243 |
-
HumanMessage(content=f"Evaluate this answer: {last_message.content}")
|
244 |
-
]
|
245 |
-
|
246 |
-
# Get evaluation
|
247 |
-
evaluation = evaluator_llm.invoke(evaluation_message)
|
248 |
-
print(f"Evaluation result: {evaluation.content}")
|
249 |
-
|
250 |
-
# Create an AIMessage with the evaluation content
|
251 |
-
evaluation_ai_message = AIMessage(content=evaluation.content)
|
252 |
-
|
253 |
-
# Return both the evaluation message and the evaluation result
|
254 |
-
return {
|
255 |
-
"messages": state["messages"] + [evaluation_ai_message],
|
256 |
-
"evaluation_result": evaluation.content,
|
257 |
-
"evaluation_attempt_count": attempt_count + 1
|
258 |
-
}
|
259 |
-
"""
|
260 |
-
|
261 |
-
# Create the graph
|
262 |
-
def create_agent_graph() -> StateGraph:
|
263 |
-
"""Create the complete agent graph."""
|
264 |
-
builder = StateGraph(AgentState)
|
265 |
-
|
266 |
-
# Define nodes: these do the work
|
267 |
-
builder.add_node("assistant", assistant)
|
268 |
-
builder.add_node("tools", ToolNode(tools))
|
269 |
-
# builder.add_node("evaluator", evaluator) # Commented out evaluator
|
270 |
-
|
271 |
-
# Define edges: these determine how the control flow moves
|
272 |
-
builder.add_edge(START, "assistant")
|
273 |
-
|
274 |
-
# First, check if the assistant's output contains tool calls
|
275 |
-
def debug_tools_condition(state):
|
276 |
-
# Check if the last message has tool calls
|
277 |
-
last_message = state["messages"][-1]
|
278 |
-
print(f"Last message type: {type(last_message)}")
|
279 |
-
|
280 |
-
has_tool_calls = False
|
281 |
-
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
282 |
-
has_tool_calls = True
|
283 |
-
print(f"Tool calls found: {last_message.tool_calls}")
|
284 |
-
|
285 |
-
result = "tools" if has_tool_calls else None
|
286 |
-
print(f"Tools condition result: {result}")
|
287 |
-
return result
|
288 |
-
|
289 |
-
builder.add_conditional_edges(
|
290 |
-
"assistant",
|
291 |
-
debug_tools_condition,
|
292 |
-
{
|
293 |
-
"tools": "tools",
|
294 |
-
None: END # Changed from evaluator to END
|
295 |
-
}
|
296 |
-
)
|
297 |
-
|
298 |
-
# Tools always goes back to assistant
|
299 |
-
builder.add_edge("tools", "assistant")
|
300 |
-
|
301 |
-
# Add evaluation edges with attempt counter (commented out)
|
302 |
-
"""
|
303 |
-
def evaluation_condition(state: AgentState) -> str:
|
304 |
-
# Print the state keys to debug
|
305 |
-
print(f"Evaluation condition state keys: {state.keys()}")
|
306 |
-
|
307 |
-
# Get the evaluation result from the state
|
308 |
-
evaluation_result = state.get("evaluation_result", "")
|
309 |
-
print(f"Evaluation result: {evaluation_result}")
|
310 |
-
|
311 |
-
# Get the evaluation attempt count or initialize to 0
|
312 |
-
attempt_count = state.get("evaluation_attempt_count", 0)
|
313 |
-
|
314 |
-
# Increment the attempt count
|
315 |
-
attempt_count += 1
|
316 |
-
print(f"Evaluation attempt: {attempt_count}")
|
317 |
-
|
318 |
-
# If we've reached max attempts or evaluation accepts the answer, end
|
319 |
-
if attempt_count >= 3 or evaluation_result.startswith("ACCEPT"):
|
320 |
-
return "end"
|
321 |
-
else:
|
322 |
-
return "assistant"
|
323 |
-
|
324 |
-
builder.add_conditional_edges(
|
325 |
-
"evaluator",
|
326 |
-
evaluation_condition,
|
327 |
-
{
|
328 |
-
"end": END,
|
329 |
-
"assistant": "assistant"
|
330 |
-
}
|
331 |
-
)
|
332 |
-
"""
|
333 |
-
|
334 |
-
# Compile with a reasonable recursion limit to prevent infinite loops
|
335 |
-
return builder.compile()
|
336 |
-
|
337 |
-
# Main agent class that integrates with your existing app.py
|
338 |
-
class TurboNerd:
|
339 |
-
def __init__(self, max_execution_time=30):
|
340 |
-
self.graph = create_agent_graph()
|
341 |
-
self.tools = tools
|
342 |
-
self.max_execution_time = max_execution_time # Maximum execution time in seconds
|
343 |
-
|
344 |
-
def __call__(self, question: str) -> str:
|
345 |
-
"""Process a question and return an answer."""
|
346 |
-
# Initialize the state with the question
|
347 |
-
initial_state = {
|
348 |
-
"messages": [HumanMessage(content=question)],
|
349 |
-
}
|
350 |
-
|
351 |
-
# Run the graph with timeout
|
352 |
-
print(f"Starting graph execution with question: {question}")
|
353 |
-
start_time = time.time()
|
354 |
-
|
355 |
-
try:
|
356 |
-
# Set a reasonable recursion limit
|
357 |
-
result = self.graph.invoke(initial_state, config={"recursion_limit": 10})
|
358 |
-
|
359 |
-
# Print the final state for debugging
|
360 |
-
print(f"Final state keys: {result.keys()}")
|
361 |
-
print(f"Final message count: {len(result['messages'])}")
|
362 |
-
|
363 |
-
# Extract the final message
|
364 |
-
final_message = result["messages"][-1]
|
365 |
-
return final_message.content
|
366 |
-
|
367 |
-
except Exception as e:
|
368 |
-
elapsed_time = time.time() - start_time
|
369 |
-
print(f"Error after {elapsed_time:.2f} seconds: {str(e)}")
|
370 |
-
|
371 |
-
# If we've been running too long, return a timeout message
|
372 |
-
if elapsed_time > self.max_execution_time:
|
373 |
-
return f"""I wasn't able to complete the full analysis within the time limit, but here's what I found:
|
374 |
-
|
375 |
-
The population of New York City is approximately 8.8 million (as of the 2020 Census).
|
376 |
-
|
377 |
-
For a population doubling at 2% annual growth rate, it would take about 35 years. This can be calculated using the Rule of 70, which states that dividing 70 by the growth rate gives the approximate doubling time:
|
378 |
-
|
379 |
-
70 ÷ 2 = 35 years
|
380 |
-
|
381 |
-
You can verify this with a Python calculation:
|
382 |
-
```python
|
383 |
-
years = 0
|
384 |
-
population = 1
|
385 |
-
while population < 2:
|
386 |
-
population *= 1.02 # 2% growth
|
387 |
-
years += 1
|
388 |
-
print(years) # Result: 35
|
389 |
-
```"""
|
390 |
-
|
391 |
-
# Otherwise return the error
|
392 |
-
return f"I encountered an error while processing your question: {str(e)}"
|
393 |
-
|
394 |
-
# Example usage:
|
395 |
-
if __name__ == "__main__":
|
396 |
-
agent = TurboNerd(max_execution_time=30)
|
397 |
-
response = agent("What is the population of New York City? Then write a Python program to calculate how many years it would take for the population to double at a 2% annual growth rate.")
|
398 |
-
print("\nFinal Response:")
|
399 |
-
print(response)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
@@ -4,13 +4,25 @@ import requests
|
|
4 |
import inspect
|
5 |
import pandas as pd
|
6 |
import base64
|
7 |
-
from agent import
|
8 |
from rate_limiter import QueryRateLimiter
|
9 |
from flask import request
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
# --- Constants ---
|
12 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
13 |
ALLOWED_FILE_EXTENSIONS = [".mp3", ".xlsx", ".py", ".png", ".jpg", ".jpeg", ".gif", ".txt", ".md", ".json", ".csv", ".yml", ".yaml", ".html", ".css", ".js"]
|
|
|
|
|
14 |
|
15 |
# Initialize rate limiter (5 queries per hour)
|
16 |
query_limiter = QueryRateLimiter(max_queries_per_hour=5)
|
@@ -18,17 +30,10 @@ query_limiter = QueryRateLimiter(max_queries_per_hour=5)
|
|
18 |
# Dictionary to store session-specific conversation histories
|
19 |
session_histories = {}
|
20 |
|
21 |
-
# ---
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
self.agent = TurboNerd()
|
26 |
-
|
27 |
-
def __call__(self, question: str) -> str:
|
28 |
-
print(f"Agent received question (first 50 chars): {question[:50]}...")
|
29 |
-
answer = self.agent(question)
|
30 |
-
print(f"Agent returning answer: {answer[:50]}...")
|
31 |
-
return answer
|
32 |
|
33 |
# --- Chat Interface Functions ---
|
34 |
def format_history_for_agent(history: list) -> str:
|
@@ -47,11 +52,90 @@ def format_history_for_agent(history: list) -> str:
|
|
47 |
|
48 |
return "\n".join(formatted_history)
|
49 |
|
50 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
"""
|
52 |
-
Handle chat interaction with
|
53 |
"""
|
54 |
-
|
|
|
|
|
|
|
55 |
return history, ""
|
56 |
|
57 |
try:
|
@@ -62,7 +146,6 @@ def chat_with_agent(question: str, file_uploads, history: list) -> tuple:
|
|
62 |
# Initialize or get session history
|
63 |
if session_id not in session_histories:
|
64 |
session_histories[session_id] = []
|
65 |
-
# If we have existing history, add it to the session history
|
66 |
if history:
|
67 |
session_histories[session_id].extend(history)
|
68 |
|
@@ -70,8 +153,30 @@ def chat_with_agent(question: str, file_uploads, history: list) -> tuple:
|
|
70 |
history.append({"role": "user", "content": question})
|
71 |
session_histories[session_id].append({"role": "user", "content": question})
|
72 |
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
|
76 |
# Process uploaded files if any
|
77 |
attachments = {}
|
@@ -82,16 +187,26 @@ def chat_with_agent(question: str, file_uploads, history: list) -> tuple:
|
|
82 |
if file is not None:
|
83 |
file_path = file.name
|
84 |
file_name = os.path.basename(file_path)
|
85 |
-
file_ext = os.path.splitext(file_name)[1].lower()
|
86 |
|
87 |
-
#
|
88 |
-
|
89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
with open(file_path, "rb") as f:
|
91 |
file_content = f.read()
|
92 |
-
file_content_b64 = base64.b64encode(file_content).decode(
|
93 |
attachments[file_name] = file_content_b64
|
94 |
-
file_info += f"\nUploaded file: {
|
95 |
|
96 |
if file_info:
|
97 |
if question.strip():
|
@@ -101,7 +216,6 @@ def chat_with_agent(question: str, file_uploads, history: list) -> tuple:
|
|
101 |
|
102 |
# Format the session-specific conversation history
|
103 |
conversation_history = format_history_for_agent(session_histories[session_id])
|
104 |
-
print(f"Current conversation history:\n{conversation_history}") # Debug print
|
105 |
|
106 |
# Prepare the full context for the agent
|
107 |
full_context = f"Question: {question}\n\nConversation History:\n{conversation_history}"
|
@@ -115,25 +229,23 @@ def chat_with_agent(question: str, file_uploads, history: list) -> tuple:
|
|
115 |
# Format the response to show thought process
|
116 |
formatted_response = ""
|
117 |
if "Thought:" in response:
|
118 |
-
# Split the response into sections
|
119 |
sections = response.split("\n\n")
|
120 |
for section in sections:
|
121 |
if section.startswith("Thought:"):
|
122 |
-
formatted_response += f"
|
123 |
elif section.startswith("Action:"):
|
124 |
-
# Extract the tool being used
|
125 |
if "action" in section and "action_input" in section:
|
126 |
try:
|
127 |
import json
|
128 |
action_json = json.loads(section.split("```json")[1].split("```")[0].strip())
|
129 |
tool_name = action_json.get("action", "").replace("_", " ").title()
|
130 |
-
formatted_response += f"
|
131 |
except:
|
132 |
-
formatted_response += f"
|
133 |
elif section.startswith("Observation:"):
|
134 |
-
formatted_response += f"
|
135 |
elif section.startswith("Final Answer:"):
|
136 |
-
formatted_response += f"
|
137 |
else:
|
138 |
formatted_response += f"{section}\n\n"
|
139 |
else:
|
@@ -144,23 +256,15 @@ def chat_with_agent(question: str, file_uploads, history: list) -> tuple:
|
|
144 |
session_histories[session_id].append({"role": "assistant", "content": formatted_response})
|
145 |
|
146 |
return history, ""
|
147 |
-
|
148 |
-
error_message = (
|
149 |
-
"I apologize, but I've reached my thinking limit while trying to answer your question. "
|
150 |
-
"This usually happens when the question requires too many steps to solve and therefore too much money"
|
151 |
-
"Could you please try breaking down your question into smaller, more specific parts? "
|
152 |
-
"For example, instead of asking about multiple things at once, try asking about one aspect at a time."
|
153 |
-
)
|
154 |
-
history.append({"role": "assistant", "content": error_message})
|
155 |
-
if session_id in session_histories:
|
156 |
-
session_histories[session_id].append({"role": "assistant", "content": error_message})
|
157 |
-
return history, ""
|
158 |
except Exception as e:
|
159 |
error_str = str(e).lower()
|
160 |
-
if "credit" in error_str or "quota" in error_str or "limit" in error_str or "exceeded" in error_str
|
161 |
-
error_message =
|
162 |
-
|
163 |
-
|
|
|
|
|
164 |
else:
|
165 |
error_message = f"Error: {str(e)}"
|
166 |
|
@@ -176,7 +280,7 @@ def clear_chat():
|
|
176 |
# --- Evaluation Functions ---
|
177 |
def run_and_submit_all(profile: gr.OAuthProfile | None):
|
178 |
"""
|
179 |
-
Fetches all questions, runs the
|
180 |
and displays the results.
|
181 |
"""
|
182 |
# --- Determine HF Space Runtime URL and Repo URL ---
|
@@ -193,9 +297,14 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
193 |
questions_url = f"{api_url}/questions"
|
194 |
submit_url = f"{api_url}/submit"
|
195 |
|
196 |
-
# 1. Instantiate Agent
|
197 |
try:
|
198 |
-
agent =
|
|
|
|
|
|
|
|
|
|
|
199 |
except Exception as e:
|
200 |
print(f"Error instantiating agent: {e}")
|
201 |
return f"Error initializing agent: {e}", None
|
@@ -312,133 +421,143 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
312 |
return status_message, results_df
|
313 |
|
314 |
# --- Build Gradio Interface using Blocks with Tabs ---
|
315 |
-
with gr.Blocks(title="
|
316 |
-
gr.
|
317 |
-
|
318 |
-
|
319 |
-
# Tab 1: Chat Interface
|
320 |
-
with gr.TabItem("🤓", id="chat"):
|
321 |
gr.Markdown("""
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
327 |
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
|
|
|
|
348 |
with gr.Row():
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
with gr.Row():
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
file_types=ALLOWED_FILE_EXTENSIONS,
|
371 |
-
file_count="multiple",
|
372 |
-
scale=1
|
373 |
-
)
|
374 |
-
with gr.Row():
|
375 |
-
submit_btn = gr.Button("Send", variant="primary")
|
376 |
-
|
377 |
-
# Chat interface event handlers
|
378 |
-
submit_btn.click(
|
379 |
-
fn=chat_with_agent,
|
380 |
-
inputs=[question_input, file_upload, chatbot],
|
381 |
-
outputs=[chatbot, question_input]
|
382 |
-
)
|
383 |
-
|
384 |
-
question_input.submit(
|
385 |
-
fn=chat_with_agent,
|
386 |
-
inputs=[question_input, file_upload, chatbot],
|
387 |
-
outputs=[chatbot, question_input]
|
388 |
-
)
|
389 |
|
390 |
-
#
|
391 |
-
with gr.
|
392 |
-
gr.Markdown(""
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
421 |
|
422 |
if __name__ == "__main__":
|
423 |
print("\n" + "-"*30 + " App Starting " + "-"*30)
|
424 |
-
# Check for SPACE_HOST and SPACE_ID at startup for information
|
425 |
space_host_startup = os.getenv("SPACE_HOST")
|
426 |
-
space_id_startup = os.getenv("SPACE_ID")
|
427 |
|
428 |
if space_host_startup:
|
429 |
-
print(f"
|
430 |
print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
|
431 |
else:
|
432 |
-
print("
|
433 |
|
434 |
-
if space_id_startup:
|
435 |
-
print(f"
|
436 |
print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
|
437 |
print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
|
438 |
else:
|
439 |
-
print("
|
440 |
|
441 |
print("-"*(60 + len(" App Starting ")) + "\n")
|
442 |
|
443 |
-
print("Launching Gradio Interface for
|
444 |
demo.launch(debug=True, share=False, show_api=False, favicon_path="static/favicon.ico", enable_monitoring=True)
|
|
|
4 |
import inspect
|
5 |
import pandas as pd
|
6 |
import base64
|
7 |
+
from agent import ScholarAI
|
8 |
from rate_limiter import QueryRateLimiter
|
9 |
from flask import request
|
10 |
+
import PyPDF2
|
11 |
+
import fitz # PyMuPDF
|
12 |
+
import time
|
13 |
+
from typing import List, Tuple, Optional
|
14 |
+
from langchain_openai import ChatOpenAI
|
15 |
+
from langchain_core.messages import HumanMessage, AIMessage
|
16 |
+
|
17 |
+
# Load custom CSS
|
18 |
+
with open("static/custom.css", "r", encoding="utf-8") as f:
|
19 |
+
custom_css = f.read()
|
20 |
|
21 |
# --- Constants ---
|
22 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
23 |
ALLOWED_FILE_EXTENSIONS = [".mp3", ".xlsx", ".py", ".png", ".jpg", ".jpeg", ".gif", ".txt", ".md", ".json", ".csv", ".yml", ".yaml", ".html", ".css", ".js"]
|
24 |
+
MAX_FILE_SIZE_MB = 10
|
25 |
+
CHUNK_SIZE = 1000 # characters per chunk for text processing
|
26 |
|
27 |
# Initialize rate limiter (5 queries per hour)
|
28 |
query_limiter = QueryRateLimiter(max_queries_per_hour=5)
|
|
|
30 |
# Dictionary to store session-specific conversation histories
|
31 |
session_histories = {}
|
32 |
|
33 |
+
# --- Model Settings ---
|
34 |
+
DEFAULT_TEMPERATURE = 0.1
|
35 |
+
DEFAULT_MAX_TOKENS = 2000
|
36 |
+
DEFAULT_MODEL = "gpt-4o-mini"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
# --- Chat Interface Functions ---
|
39 |
def format_history_for_agent(history: list) -> str:
|
|
|
52 |
|
53 |
return "\n".join(formatted_history)
|
54 |
|
55 |
+
def validate_inputs(question: str, file_uploads: List[gr.File]) -> Tuple[bool, str]:
|
56 |
+
"""Validate user inputs before processing."""
|
57 |
+
if not question.strip() and (not file_uploads or len(file_uploads) == 0):
|
58 |
+
return False, "Please enter a question or upload a file."
|
59 |
+
|
60 |
+
if len(question) > 2000:
|
61 |
+
return False, "Question is too long. Please keep it under 2000 characters."
|
62 |
+
|
63 |
+
if file_uploads:
|
64 |
+
for file in file_uploads:
|
65 |
+
if file is None:
|
66 |
+
continue
|
67 |
+
|
68 |
+
file_path = file.name
|
69 |
+
if not os.path.exists(file_path):
|
70 |
+
return False, f"File {os.path.basename(file_path)} not found."
|
71 |
+
|
72 |
+
file_size = os.path.getsize(file_path) / (1024 * 1024) # Convert to MB
|
73 |
+
|
74 |
+
if file_size > MAX_FILE_SIZE_MB:
|
75 |
+
return False, f"File {os.path.basename(file_path)} is too large. Maximum size is {MAX_FILE_SIZE_MB}MB."
|
76 |
+
|
77 |
+
file_ext = os.path.splitext(file_path)[1].lower()
|
78 |
+
if file_ext not in ALLOWED_FILE_EXTENSIONS:
|
79 |
+
return False, f"File {os.path.basename(file_path)} has an unsupported format. Allowed formats: {', '.join(ALLOWED_FILE_EXTENSIONS)}"
|
80 |
+
|
81 |
+
return True, ""
|
82 |
+
|
83 |
+
def process_document(file_path: str, progress=gr.Progress()) -> List[str]:
|
84 |
+
"""Process document and return chunks with progress bar."""
|
85 |
+
file_ext = os.path.splitext(file_path)[1].lower()
|
86 |
+
chunks = []
|
87 |
+
|
88 |
+
try:
|
89 |
+
if file_ext == '.pdf':
|
90 |
+
# Process PDF
|
91 |
+
doc = fitz.open(file_path)
|
92 |
+
total_pages = len(doc)
|
93 |
+
|
94 |
+
for page_num in progress.tqdm(range(total_pages), desc="Processing PDF pages"):
|
95 |
+
page = doc[page_num]
|
96 |
+
text = page.get_text()
|
97 |
+
# Split text into chunks
|
98 |
+
for i in range(0, len(text), CHUNK_SIZE):
|
99 |
+
chunk = text[i:i + CHUNK_SIZE]
|
100 |
+
if chunk.strip():
|
101 |
+
chunks.append(f"[Page {page_num + 1}] {chunk}")
|
102 |
+
time.sleep(0.1) # Small delay to show progress
|
103 |
+
|
104 |
+
elif file_ext in ['.txt', '.md', '.json', '.csv', '.yml', '.yaml', '.html', '.css', '.js', '.py']:
|
105 |
+
# Process text files
|
106 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
107 |
+
text = f.read()
|
108 |
+
total_chunks = len(text) // CHUNK_SIZE + (1 if len(text) % CHUNK_SIZE else 0)
|
109 |
+
|
110 |
+
for i in progress.tqdm(range(0, len(text), CHUNK_SIZE), desc="Processing text chunks"):
|
111 |
+
chunk = text[i:i + CHUNK_SIZE]
|
112 |
+
if chunk.strip():
|
113 |
+
chunks.append(chunk)
|
114 |
+
time.sleep(0.1) # Small delay to show progress
|
115 |
+
|
116 |
+
elif file_ext in ['.xlsx']:
|
117 |
+
# Process Excel files
|
118 |
+
df = pd.read_excel(file_path)
|
119 |
+
total_rows = len(df)
|
120 |
+
|
121 |
+
for i in progress.tqdm(range(0, total_rows, CHUNK_SIZE), desc="Processing Excel rows"):
|
122 |
+
chunk_df = df.iloc[i:i + CHUNK_SIZE]
|
123 |
+
chunks.append(chunk_df.to_string())
|
124 |
+
time.sleep(0.1) # Small delay to show progress
|
125 |
+
|
126 |
+
return chunks
|
127 |
+
|
128 |
+
except Exception as e:
|
129 |
+
return [f"Error processing file: {str(e)}"]
|
130 |
+
|
131 |
+
def chat_with_agent(question: str, file_uploads, history: list, temperature: float, max_tokens: int, model: str, progress=gr.Progress()) -> tuple:
|
132 |
"""
|
133 |
+
Handle chat interaction with ScholarAI agent, now with file upload support and input validation.
|
134 |
"""
|
135 |
+
# Validate inputs
|
136 |
+
is_valid, error_message = validate_inputs(question, file_uploads)
|
137 |
+
if not is_valid:
|
138 |
+
history.append({"role": "assistant", "content": f"❌ {error_message}"})
|
139 |
return history, ""
|
140 |
|
141 |
try:
|
|
|
146 |
# Initialize or get session history
|
147 |
if session_id not in session_histories:
|
148 |
session_histories[session_id] = []
|
|
|
149 |
if history:
|
150 |
session_histories[session_id].extend(history)
|
151 |
|
|
|
153 |
history.append({"role": "user", "content": question})
|
154 |
session_histories[session_id].append({"role": "user", "content": question})
|
155 |
|
156 |
+
try:
|
157 |
+
# Initialize agent with current settings
|
158 |
+
agent = ScholarAI(
|
159 |
+
max_iterations=35,
|
160 |
+
temperature=temperature,
|
161 |
+
max_tokens=max_tokens,
|
162 |
+
model=model
|
163 |
+
)
|
164 |
+
print("Agent initialized successfully with Temperature: ", temperature, "Max Tokens: ", max_tokens, "Model: ", model)
|
165 |
+
except ValueError as e:
|
166 |
+
error_message = str(e)
|
167 |
+
if "API key not found" in error_message:
|
168 |
+
error_message = "OpenAI API key not found. Please set the OPENAI_API_KEY environment variable."
|
169 |
+
elif "Invalid OpenAI API key" in error_message:
|
170 |
+
error_message = "Invalid OpenAI API key. Please check your API key and try again."
|
171 |
+
elif "rate limit" in error_message.lower() or "quota" in error_message.lower():
|
172 |
+
error_message = "OpenAI API rate limit exceeded or quota reached. Please try again later."
|
173 |
+
else:
|
174 |
+
error_message = f"Error initializing AI agent: {error_message}"
|
175 |
+
|
176 |
+
history.append({"role": "assistant", "content": error_message})
|
177 |
+
if session_id in session_histories:
|
178 |
+
session_histories[session_id].append({"role": "assistant", "content": error_message})
|
179 |
+
return history, ""
|
180 |
|
181 |
# Process uploaded files if any
|
182 |
attachments = {}
|
|
|
187 |
if file is not None:
|
188 |
file_path = file.name
|
189 |
file_name = os.path.basename(file_path)
|
|
|
190 |
|
191 |
+
# Process document and get chunks
|
192 |
+
chunks = process_document(file_path, progress)
|
193 |
+
|
194 |
+
if len(chunks) > 1:
|
195 |
+
file_info += f"\nProcessing {file_name} in {len(chunks)} chunks..."
|
196 |
+
|
197 |
+
# Process each chunk
|
198 |
+
for i, chunk in enumerate(chunks, 1):
|
199 |
+
chunk_name = f"{file_name}_chunk_{i}"
|
200 |
+
chunk_content = base64.b64encode(chunk.encode('utf-8')).decode('utf-8')
|
201 |
+
attachments[chunk_name] = chunk_content
|
202 |
+
file_info += f"\nProcessed chunk {i}/{len(chunks)}"
|
203 |
+
else:
|
204 |
+
# Single chunk or error
|
205 |
with open(file_path, "rb") as f:
|
206 |
file_content = f.read()
|
207 |
+
file_content_b64 = base64.b64encode(file_content).decode('utf-8')
|
208 |
attachments[file_name] = file_content_b64
|
209 |
+
file_info += f"\nUploaded file: {file_name}"
|
210 |
|
211 |
if file_info:
|
212 |
if question.strip():
|
|
|
216 |
|
217 |
# Format the session-specific conversation history
|
218 |
conversation_history = format_history_for_agent(session_histories[session_id])
|
|
|
219 |
|
220 |
# Prepare the full context for the agent
|
221 |
full_context = f"Question: {question}\n\nConversation History:\n{conversation_history}"
|
|
|
229 |
# Format the response to show thought process
|
230 |
formatted_response = ""
|
231 |
if "Thought:" in response:
|
|
|
232 |
sections = response.split("\n\n")
|
233 |
for section in sections:
|
234 |
if section.startswith("Thought:"):
|
235 |
+
formatted_response += f"{section[7:].strip()}\n\n"
|
236 |
elif section.startswith("Action:"):
|
|
|
237 |
if "action" in section and "action_input" in section:
|
238 |
try:
|
239 |
import json
|
240 |
action_json = json.loads(section.split("```json")[1].split("```")[0].strip())
|
241 |
tool_name = action_json.get("action", "").replace("_", " ").title()
|
242 |
+
formatted_response += f"Using {tool_name}...\n\n"
|
243 |
except:
|
244 |
+
formatted_response += f"{section[7:].strip()}\n\n"
|
245 |
elif section.startswith("Observation:"):
|
246 |
+
formatted_response += f"{section[11:].strip()}\n\n"
|
247 |
elif section.startswith("Final Answer:"):
|
248 |
+
formatted_response += f"{section[12:].strip()}\n\n"
|
249 |
else:
|
250 |
formatted_response += f"{section}\n\n"
|
251 |
else:
|
|
|
256 |
session_histories[session_id].append({"role": "assistant", "content": formatted_response})
|
257 |
|
258 |
return history, ""
|
259 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
260 |
except Exception as e:
|
261 |
error_str = str(e).lower()
|
262 |
+
if "credit" in error_str or "quota" in error_str or "limit" in error_str or "exceeded" in error_str:
|
263 |
+
error_message = "It seems I've run out of API credits. Please try again later or tomorrow when the credits reset."
|
264 |
+
elif "invalid_api_key" in error_str or "incorrect_api_key" in error_str:
|
265 |
+
error_message = "Invalid OpenAI API key. Please check your API key and try again."
|
266 |
+
elif "api_key" in error_str:
|
267 |
+
error_message = "OpenAI API key not found. Please set the OPENAI_API_KEY environment variable."
|
268 |
else:
|
269 |
error_message = f"Error: {str(e)}"
|
270 |
|
|
|
280 |
# --- Evaluation Functions ---
|
281 |
def run_and_submit_all(profile: gr.OAuthProfile | None):
|
282 |
"""
|
283 |
+
Fetches all questions, runs the ScholarAI on them, submits all answers,
|
284 |
and displays the results.
|
285 |
"""
|
286 |
# --- Determine HF Space Runtime URL and Repo URL ---
|
|
|
297 |
questions_url = f"{api_url}/questions"
|
298 |
submit_url = f"{api_url}/submit"
|
299 |
|
300 |
+
# 1. Instantiate Agent
|
301 |
try:
|
302 |
+
agent = ScholarAI(
|
303 |
+
max_iterations=35,
|
304 |
+
temperature=DEFAULT_TEMPERATURE,
|
305 |
+
max_tokens=DEFAULT_MAX_TOKENS,
|
306 |
+
model=DEFAULT_MODEL
|
307 |
+
)
|
308 |
except Exception as e:
|
309 |
print(f"Error instantiating agent: {e}")
|
310 |
return f"Error initializing agent: {e}", None
|
|
|
421 |
return status_message, results_df
|
422 |
|
423 |
# --- Build Gradio Interface using Blocks with Tabs ---
|
424 |
+
with gr.Blocks(title="ScholarAI Agent", css=custom_css) as demo:
|
425 |
+
with gr.Row(elem_classes="header-bar"):
|
426 |
+
with gr.Column(scale=3):
|
427 |
+
gr.Markdown("# <span style='font-size: 1.8em'>ScholarAI</span>", elem_classes="title")
|
|
|
|
|
428 |
gr.Markdown("""
|
429 |
+
<div class="badges">
|
430 |
+
<img src="https://img.shields.io/badge/build-passing-brightgreen" alt="Build Status">
|
431 |
+
<img src="https://img.shields.io/badge/License-MIT-yellow" alt="License">
|
432 |
+
<img src="https://img.shields.io/badge/version-1.0.0-blue" alt="Version">
|
433 |
+
<img src="https://img.shields.io/badge/python-3.11-blue" alt="Python">
|
434 |
+
<img src="https://img.shields.io/badge/gradio-5.29.1-orange" alt="Gradio">
|
435 |
+
</div>
|
436 |
+
""", elem_classes="badges-container")
|
437 |
+
with gr.Column(scale=1):
|
438 |
+
gr.Markdown("<span style='font-size: 0.9em'>by [Vividh Mahajan](https://huggingface.co/Lasdw)</span>", elem_classes="author")
|
439 |
+
|
440 |
+
gr.Markdown("""
|
441 |
+
## ScholarAI helps you find answers by searching the web, analyzing images, processing audio, and more.
|
442 |
+
|
443 |
+
### Tip: Ask specific, factual questions for best results. Some websites may be restricted.
|
444 |
+
""")
|
445 |
+
|
446 |
+
with gr.Accordion("Example Questions", open=False, elem_classes="example-questions"):
|
447 |
+
gr.Markdown("""
|
448 |
+
### Example Questions:
|
449 |
|
450 |
+
**Research & Analysis:**
|
451 |
+
- "Find the first name of the only Malko Competition recipient from the 20th Century (after 1977) whose nationality on record is a country that no longer exists. Tell me thier current age and where they are from."
|
452 |
+
- "Analyze this image of a mathematical equation, and find an academic papers that use this equation."
|
453 |
|
454 |
+
**Multi-Modal Analysis:**
|
455 |
+
- "I have an interview recording and a transcript. Compare the audio transcription with the provided transcript, identify any discrepancies."
|
456 |
+
- "This image shows a historical document. Find me the historical events from that era."
|
457 |
|
458 |
+
**Code & Data Processing:**
|
459 |
+
- "I have a Python script and an Excel file with data. Analyze the code's functionality and suggest improvements based on the data patterns."
|
460 |
+
- "This code contains a bug. Debug it."
|
461 |
|
462 |
+
The agent can handle multiple file uploads and combine information from various sources to provide comprehensive answers. Try asking complex questions that require multiple tools working together!
|
463 |
+
""")
|
464 |
+
|
465 |
+
with gr.Row():
|
466 |
+
# Left panel - Chat interface
|
467 |
+
with gr.Column(scale=2):
|
468 |
+
chatbot = gr.Chatbot(
|
469 |
+
height=250,
|
470 |
+
type="messages"
|
471 |
+
)
|
472 |
with gr.Row():
|
473 |
+
question_input = gr.Textbox(
|
474 |
+
label="Ask a question",
|
475 |
+
placeholder="e.g. Analyze this interview transcript and find discrepancies",
|
476 |
+
lines=5,
|
477 |
+
max_lines=5,
|
478 |
+
container=True,
|
479 |
+
scale=2,
|
480 |
+
min_width=500
|
481 |
+
)
|
482 |
+
with gr.Column(scale=1):
|
483 |
with gr.Row():
|
484 |
+
with gr.Column(scale=2):
|
485 |
+
file_upload = gr.File(
|
486 |
+
label="Upload Files (.png, .txt, .mp3, .xlsx, .py)",
|
487 |
+
file_types=ALLOWED_FILE_EXTENSIONS,
|
488 |
+
file_count="multiple",
|
489 |
+
height=175,
|
490 |
+
min_width=200
|
491 |
+
)
|
492 |
+
with gr.Row():
|
493 |
+
submit_btn = gr.Button("Start Research", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
494 |
|
495 |
+
# Right panel - Controls
|
496 |
+
with gr.Column(scale=1):
|
497 |
+
gr.Markdown("# Model Settings")
|
498 |
+
with gr.Group():
|
499 |
+
temperature = gr.Slider(
|
500 |
+
minimum=0.0,
|
501 |
+
maximum=1.0,
|
502 |
+
value=DEFAULT_TEMPERATURE,
|
503 |
+
step=0.1,
|
504 |
+
label="Temperature",
|
505 |
+
info="Higher values make the output more random, lower values make it more deterministic"
|
506 |
+
)
|
507 |
+
max_tokens = gr.Slider(
|
508 |
+
minimum=100,
|
509 |
+
maximum=4000,
|
510 |
+
value=DEFAULT_MAX_TOKENS,
|
511 |
+
step=100,
|
512 |
+
label="Max Tokens",
|
513 |
+
info="Maximum length of the response"
|
514 |
+
)
|
515 |
+
model = gr.Dropdown(
|
516 |
+
choices=["gpt-4o-mini", "gpt-3.5-turbo"],
|
517 |
+
value=DEFAULT_MODEL,
|
518 |
+
label="Model",
|
519 |
+
info="The language model to use"
|
520 |
+
)
|
521 |
+
|
522 |
+
# Footer with disclaimer
|
523 |
+
gr.Markdown("""
|
524 |
+
<div class="footer">
|
525 |
+
This tool is designed for educational and research purposes only. It is not intended for malicious use.
|
526 |
+
</div>
|
527 |
+
""")
|
528 |
+
|
529 |
+
# Chat interface event handlers
|
530 |
+
submit_btn.click(
|
531 |
+
fn=chat_with_agent,
|
532 |
+
inputs=[question_input, file_upload, chatbot, temperature, max_tokens, model],
|
533 |
+
outputs=[chatbot, question_input]
|
534 |
+
)
|
535 |
+
|
536 |
+
question_input.submit(
|
537 |
+
fn=chat_with_agent,
|
538 |
+
inputs=[question_input, file_upload, chatbot, temperature, max_tokens, model],
|
539 |
+
outputs=[chatbot, question_input]
|
540 |
+
)
|
541 |
|
542 |
if __name__ == "__main__":
|
543 |
print("\n" + "-"*30 + " App Starting " + "-"*30)
|
|
|
544 |
space_host_startup = os.getenv("SPACE_HOST")
|
545 |
+
space_id_startup = os.getenv("SPACE_ID")
|
546 |
|
547 |
if space_host_startup:
|
548 |
+
print(f"SPACE_HOST found: {space_host_startup}")
|
549 |
print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
|
550 |
else:
|
551 |
+
print("SPACE_HOST environment variable not found (running locally?).")
|
552 |
|
553 |
+
if space_id_startup:
|
554 |
+
print(f"SPACE_ID found: {space_id_startup}")
|
555 |
print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
|
556 |
print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
|
557 |
else:
|
558 |
+
print("SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
|
559 |
|
560 |
print("-"*(60 + len(" App Starting ")) + "\n")
|
561 |
|
562 |
+
print("Launching Gradio Interface for ScholarAI Agent...")
|
563 |
demo.launch(debug=True, share=False, show_api=False, favicon_path="static/favicon.ico", enable_monitoring=True)
|
static/custom.css
ADDED
@@ -0,0 +1,478 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
:root {
|
2 |
+
--primary-purple: #8a3db8;
|
3 |
+
--dark-purple: #6a2c9e;
|
4 |
+
--light-purple: #b366ff;
|
5 |
+
--black: #1a1a1a;
|
6 |
+
--dark-gray: #2d2d2d;
|
7 |
+
--light-gray: #404040;
|
8 |
+
--white: #ffffff;
|
9 |
+
}
|
10 |
+
|
11 |
+
/* Header styling */
|
12 |
+
h1 {
|
13 |
+
font-size: 2.5em !important;
|
14 |
+
margin-bottom: 0.5em !important;
|
15 |
+
}
|
16 |
+
|
17 |
+
h1 span {
|
18 |
+
color: var(--light-purple) !important;
|
19 |
+
font-weight: bold !important;
|
20 |
+
}
|
21 |
+
|
22 |
+
/* Global styles */
|
23 |
+
.gradio-container {
|
24 |
+
background-color: var(--black) !important;
|
25 |
+
color: var(--white) !important;
|
26 |
+
padding-bottom: 60px !important;
|
27 |
+
}
|
28 |
+
|
29 |
+
/* Chat interface */
|
30 |
+
.chatbot {
|
31 |
+
background-color: var(--dark-gray) !important;
|
32 |
+
border-radius: 8px !important;
|
33 |
+
max-width: 520px !important; /* Limit width of the whole chat area */
|
34 |
+
margin-left: auto !important;
|
35 |
+
margin-right: auto !important;
|
36 |
+
}
|
37 |
+
|
38 |
+
/* Message bubbles */
|
39 |
+
.message, .user-message, .assistant-message {
|
40 |
+
border: none !important;
|
41 |
+
border-radius: 10px !important;
|
42 |
+
box-shadow: none !important;
|
43 |
+
background: var(--light-purple) !important;
|
44 |
+
color: var(--white) !important;
|
45 |
+
margin: 8px 0 !important;
|
46 |
+
padding: 12px 18px !important;
|
47 |
+
max-width: 85% !important; /* Limit width of each message bubble */
|
48 |
+
word-break: break-word !important;
|
49 |
+
}
|
50 |
+
|
51 |
+
.user-message {
|
52 |
+
background-color: var(--light-purple) !important;
|
53 |
+
color: var(--white) !important;
|
54 |
+
}
|
55 |
+
|
56 |
+
.assistant-message {
|
57 |
+
background-color: var(--light-gray) !important;
|
58 |
+
color: var(--white) !important;
|
59 |
+
}
|
60 |
+
|
61 |
+
/* Input area */
|
62 |
+
.textbox {
|
63 |
+
background-color: var(--dark-gray) !important;
|
64 |
+
color: var(--white) !important;
|
65 |
+
}
|
66 |
+
|
67 |
+
.textbox:focus {
|
68 |
+
border-color: var(--light-purple) !important;
|
69 |
+
box-shadow: 0 0 5px var(--light-purple) !important;
|
70 |
+
}
|
71 |
+
|
72 |
+
/* Buttons */
|
73 |
+
button {
|
74 |
+
background-color: var(--light-purple) !important;
|
75 |
+
color: var(--white) !important;
|
76 |
+
border: none !important;
|
77 |
+
border-radius: 4px !important;
|
78 |
+
transition: background-color 0.3s ease !important;
|
79 |
+
}
|
80 |
+
|
81 |
+
button:hover {
|
82 |
+
background-color: var(--primary-purple) !important;
|
83 |
+
}
|
84 |
+
|
85 |
+
/* Slider styling */
|
86 |
+
input[type="range"],
|
87 |
+
.gr-form,
|
88 |
+
.gr-box,
|
89 |
+
.gr-input {
|
90 |
+
-webkit-appearance: none !important;
|
91 |
+
background: var(--light-purple) !important;
|
92 |
+
height: 4px !important;
|
93 |
+
border-radius: 2px !important;
|
94 |
+
}
|
95 |
+
|
96 |
+
input[type="range"]::-webkit-slider-thumb,
|
97 |
+
.gr-form::-webkit-slider-thumb,
|
98 |
+
.gr-box::-webkit-slider-thumb,
|
99 |
+
.gr-input::-webkit-slider-thumb {
|
100 |
+
-webkit-appearance: none !important;
|
101 |
+
width: 16px !important;
|
102 |
+
height: 16px !important;
|
103 |
+
background: var(--light-purple) !important;
|
104 |
+
border: 2px solid var(--white) !important;
|
105 |
+
border-radius: 50% !important;
|
106 |
+
cursor: pointer !important;
|
107 |
+
}
|
108 |
+
|
109 |
+
input[type="range"]::-moz-range-thumb,
|
110 |
+
.gr-form::-moz-range-thumb,
|
111 |
+
.gr-box::-moz-range-thumb,
|
112 |
+
.gr-input::-moz-range-thumb {
|
113 |
+
width: 16px !important;
|
114 |
+
height: 16px !important;
|
115 |
+
background: var(--light-purple) !important;
|
116 |
+
border: 2px solid var(--white) !important;
|
117 |
+
border-radius: 50% !important;
|
118 |
+
cursor: pointer !important;
|
119 |
+
}
|
120 |
+
|
121 |
+
input[type="range"]::-webkit-slider-runnable-track,
|
122 |
+
.gr-form::-webkit-slider-runnable-track,
|
123 |
+
.gr-box::-webkit-slider-runnable-track,
|
124 |
+
.gr-input::-webkit-slider-runnable-track {
|
125 |
+
background: var(--light-purple) !important;
|
126 |
+
height: 4px !important;
|
127 |
+
border-radius: 2px !important;
|
128 |
+
}
|
129 |
+
|
130 |
+
input[type="range"]::-moz-range-track,
|
131 |
+
.gr-form::-moz-range-track,
|
132 |
+
.gr-box::-moz-range-track,
|
133 |
+
.gr-input::-moz-range-track {
|
134 |
+
background: var(--light-purple) !important;
|
135 |
+
height: 4px !important;
|
136 |
+
border-radius: 2px !important;
|
137 |
+
}
|
138 |
+
|
139 |
+
/* Dropdown styling */
|
140 |
+
select, .dropdown {
|
141 |
+
background-color: var(--dark-gray) !important;
|
142 |
+
color: var(--white) !important;
|
143 |
+
border: 1px solid var(--light-purple) !important;
|
144 |
+
border-radius: 4px !important;
|
145 |
+
}
|
146 |
+
|
147 |
+
select:hover, .dropdown:hover {
|
148 |
+
border-color: var(--light-purple) !important;
|
149 |
+
}
|
150 |
+
|
151 |
+
select:focus, .dropdown:focus {
|
152 |
+
border-color: var(--light-purple) !important;
|
153 |
+
box-shadow: 0 0 5px var(--light-purple) !important;
|
154 |
+
}
|
155 |
+
|
156 |
+
/* Markdown content */
|
157 |
+
.markdown {
|
158 |
+
color: var(--white) !important;
|
159 |
+
}
|
160 |
+
|
161 |
+
.markdown h1, .markdown h2, .markdown h3 {
|
162 |
+
color: var(--light-purple) !important;
|
163 |
+
}
|
164 |
+
|
165 |
+
/* File upload area */
|
166 |
+
.upload-area {
|
167 |
+
background-color: var(--dark-gray) !important;
|
168 |
+
border: 2px dashed var(--light-purple) !important;
|
169 |
+
border-radius: 8px !important;
|
170 |
+
}
|
171 |
+
|
172 |
+
.upload-area:hover {
|
173 |
+
border-color: var(--light-purple) !important;
|
174 |
+
}
|
175 |
+
|
176 |
+
/* Scrollbars */
|
177 |
+
::-webkit-scrollbar {
|
178 |
+
width: 8px;
|
179 |
+
height: 8px;
|
180 |
+
}
|
181 |
+
|
182 |
+
::-webkit-scrollbar-track {
|
183 |
+
background: var(--dark-gray);
|
184 |
+
}
|
185 |
+
|
186 |
+
::-webkit-scrollbar-thumb {
|
187 |
+
background: var(--light-purple);
|
188 |
+
border-radius: 4px;
|
189 |
+
}
|
190 |
+
|
191 |
+
::-webkit-scrollbar-thumb:hover {
|
192 |
+
background: var(--primary-purple);
|
193 |
+
}
|
194 |
+
|
195 |
+
/* Footer styling */
|
196 |
+
.footer {
|
197 |
+
position: fixed;
|
198 |
+
bottom: 0;
|
199 |
+
left: 0;
|
200 |
+
right: 0;
|
201 |
+
background-color: var(--black) !important;
|
202 |
+
color: var(--white) !important;
|
203 |
+
padding: 10px 20px !important;
|
204 |
+
text-align: center !important;
|
205 |
+
border-top: 1px solid var(--light-purple) !important;
|
206 |
+
font-size: 1em !important;
|
207 |
+
z-index: 1000 !important;
|
208 |
+
}
|
209 |
+
|
210 |
+
/* Accordion styling */
|
211 |
+
.accordion {
|
212 |
+
background-color: var(--dark-gray) !important;
|
213 |
+
border: 1px solid var(--light-purple) !important;
|
214 |
+
border-radius: 8px !important;
|
215 |
+
margin: 10px 0 !important;
|
216 |
+
}
|
217 |
+
|
218 |
+
.accordion-header {
|
219 |
+
background-color: var(--dark-gray) !important;
|
220 |
+
color: var(--white) !important;
|
221 |
+
padding: 12px !important;
|
222 |
+
cursor: pointer !important;
|
223 |
+
transition: background-color 0.3s ease !important;
|
224 |
+
}
|
225 |
+
|
226 |
+
.accordion-header:hover {
|
227 |
+
background-color: var(--light-gray) !important;
|
228 |
+
}
|
229 |
+
|
230 |
+
.accordion-content {
|
231 |
+
background-color: var(--dark-gray) !important;
|
232 |
+
color: var(--white) !important;
|
233 |
+
padding: 15px !important;
|
234 |
+
border-top: 1px solid var(--light-purple) !important;
|
235 |
+
}
|
236 |
+
|
237 |
+
/* Accordion icon */
|
238 |
+
.accordion-header::after {
|
239 |
+
color: var(--light-purple) !important;
|
240 |
+
}
|
241 |
+
|
242 |
+
/* Model Settings styling */
|
243 |
+
.slider,
|
244 |
+
.gr-slider,
|
245 |
+
.gr-form {
|
246 |
+
background-color: var(--black) !important;
|
247 |
+
}
|
248 |
+
|
249 |
+
.slider .thumb,
|
250 |
+
.gr-slider .thumb,
|
251 |
+
.gr-form .thumb {
|
252 |
+
background-color: var(--light-purple) !important;
|
253 |
+
border: 2px solid var(--white) !important;
|
254 |
+
}
|
255 |
+
|
256 |
+
.slider .track,
|
257 |
+
.gr-slider .track,
|
258 |
+
.gr-form .track {
|
259 |
+
background-color: var(--dark-purple) !important;
|
260 |
+
}
|
261 |
+
|
262 |
+
.slider .track-fill,
|
263 |
+
.gr-slider .track-fill,
|
264 |
+
.gr-form .track-fill {
|
265 |
+
background-color: var(--light-purple) !important;
|
266 |
+
}
|
267 |
+
|
268 |
+
.slider .label,
|
269 |
+
.gr-slider .label,
|
270 |
+
.gr-form .label {
|
271 |
+
color: var(--white) !important;
|
272 |
+
}
|
273 |
+
|
274 |
+
.slider .info,
|
275 |
+
.gr-slider .info,
|
276 |
+
.gr-form .info,
|
277 |
+
.gr-box .info,
|
278 |
+
.gr-input .info {
|
279 |
+
color: var(--black) !important;
|
280 |
+
opacity: 1 !important;
|
281 |
+
}
|
282 |
+
|
283 |
+
/* Additional Gradio-specific overrides */
|
284 |
+
.gr-box[data-testid="slider"] .track {
|
285 |
+
background-color: var(--dark-purple) !important;
|
286 |
+
}
|
287 |
+
|
288 |
+
.gr-box[data-testid="slider"] .info {
|
289 |
+
color: var(--black) !important;
|
290 |
+
opacity: 1 !important;
|
291 |
+
}
|
292 |
+
|
293 |
+
.gr-box[data-testid="slider"] .track-fill {
|
294 |
+
background-color: var(--light-purple) !important;
|
295 |
+
}
|
296 |
+
|
297 |
+
/* Box below model settings */
|
298 |
+
.gr-box,
|
299 |
+
.gr-box[data-testid="box"],
|
300 |
+
.gr-box[data-testid="group"] {
|
301 |
+
background-color: var(--light-purple) !important;
|
302 |
+
border-radius: 8px !important;
|
303 |
+
padding: 15px !important;
|
304 |
+
margin-top: 10px !important;
|
305 |
+
}
|
306 |
+
|
307 |
+
.gr-box *,
|
308 |
+
.gr-box[data-testid="box"] *,
|
309 |
+
.gr-box[data-testid="group"] * {
|
310 |
+
color: var(--white) !important;
|
311 |
+
}
|
312 |
+
|
313 |
+
.gr-box .markdown,
|
314 |
+
.gr-box[data-testid="box"] .markdown,
|
315 |
+
.gr-box[data-testid="group"] .markdown {
|
316 |
+
color: var(--white) !important;
|
317 |
+
background-color: transparent !important;
|
318 |
+
}
|
319 |
+
|
320 |
+
.gr-box .markdown p,
|
321 |
+
.gr-box[data-testid="box"] .markdown p,
|
322 |
+
.gr-box[data-testid="group"] .markdown p {
|
323 |
+
color: var(--white) !important;
|
324 |
+
margin: 0 !important;
|
325 |
+
background-color: transparent !important;
|
326 |
+
}
|
327 |
+
|
328 |
+
/* Override any conflicting styles */
|
329 |
+
.gr-box[data-testid="box"] .column,
|
330 |
+
.gr-box[data-testid="group"] .column {
|
331 |
+
background-color: transparent !important;
|
332 |
+
}
|
333 |
+
|
334 |
+
.gr-box[data-testid="box"] .column .markdown,
|
335 |
+
.gr-box[data-testid="group"] .column .markdown {
|
336 |
+
background-color: transparent !important;
|
337 |
+
}
|
338 |
+
|
339 |
+
/* Info text styling */
|
340 |
+
.info {
|
341 |
+
color: var(--light-purple) !important;
|
342 |
+
font-size: 0.9em !important;
|
343 |
+
font-style: italic !important;
|
344 |
+
}
|
345 |
+
|
346 |
+
/* Side panel styling */
|
347 |
+
.column {
|
348 |
+
background-color: var(--black) !important;
|
349 |
+
}
|
350 |
+
|
351 |
+
.column .markdown {
|
352 |
+
background-color: var(--light-purple) !important;
|
353 |
+
border: none !important;
|
354 |
+
}
|
355 |
+
|
356 |
+
.column .markdown p {
|
357 |
+
background-color: var(--light-purple) !important;
|
358 |
+
border: none !important;
|
359 |
+
}
|
360 |
+
|
361 |
+
.header-bar {
|
362 |
+
padding: 5px 10px !important;
|
363 |
+
margin-bottom: 10px !important;
|
364 |
+
background: transparent !important;
|
365 |
+
}
|
366 |
+
|
367 |
+
.title {
|
368 |
+
text-align: left !important;
|
369 |
+
margin: 0 !important;
|
370 |
+
padding: 0 !important;
|
371 |
+
}
|
372 |
+
|
373 |
+
.author {
|
374 |
+
text-align: right !important;
|
375 |
+
margin: 0 !important;
|
376 |
+
padding: 0 !important;
|
377 |
+
line-height: 2.5em !important;
|
378 |
+
}
|
379 |
+
|
380 |
+
/* Example Questions styling */
|
381 |
+
.example-questions {
|
382 |
+
margin: 20px 0 !important;
|
383 |
+
border: 2px solid var(--light-purple) !important;
|
384 |
+
border-radius: 8px !important;
|
385 |
+
background: linear-gradient(45deg, var(--primary-purple), var(--light-purple)) !important;
|
386 |
+
transition: all 0.3s ease !important;
|
387 |
+
}
|
388 |
+
|
389 |
+
.example-questions:hover {
|
390 |
+
transform: translateY(-2px) !important;
|
391 |
+
box-shadow: 0 4px 15px rgba(156, 77, 204, 0.3) !important;
|
392 |
+
}
|
393 |
+
|
394 |
+
.example-questions .accordion-header {
|
395 |
+
background: transparent !important;
|
396 |
+
color: var(--white) !important;
|
397 |
+
font-size: 1.2em !important;
|
398 |
+
font-weight: bold !important;
|
399 |
+
padding: 15px 20px !important;
|
400 |
+
cursor: pointer !important;
|
401 |
+
display: flex !important;
|
402 |
+
align-items: center !important;
|
403 |
+
gap: 10px !important;
|
404 |
+
}
|
405 |
+
|
406 |
+
.example-questions .accordion-header::after {
|
407 |
+
content: "Click to expand" !important;
|
408 |
+
font-size: 0.8em !important;
|
409 |
+
opacity: 0.8 !important;
|
410 |
+
margin-left: auto !important;
|
411 |
+
}
|
412 |
+
|
413 |
+
.example-questions .accordion-content {
|
414 |
+
background-color: var(--dark-gray) !important;
|
415 |
+
padding: 20px !important;
|
416 |
+
border-top: 1px solid var(--light-purple) !important;
|
417 |
+
}
|
418 |
+
|
419 |
+
.example-questions .markdown {
|
420 |
+
color: var(--white) !important;
|
421 |
+
}
|
422 |
+
|
423 |
+
.example-questions .markdown h3 {
|
424 |
+
color: var(--light-purple) !important;
|
425 |
+
margin-bottom: 15px !important;
|
426 |
+
}
|
427 |
+
|
428 |
+
.example-questions .markdown strong {
|
429 |
+
color: var(--light-purple) !important;
|
430 |
+
}
|
431 |
+
|
432 |
+
/* Badges styling */
|
433 |
+
.badges-container {
|
434 |
+
margin-top: 5px !important;
|
435 |
+
}
|
436 |
+
|
437 |
+
.badges {
|
438 |
+
display: flex !important;
|
439 |
+
gap: 8px !important;
|
440 |
+
flex-wrap: wrap !important;
|
441 |
+
align-items: center !important;
|
442 |
+
}
|
443 |
+
|
444 |
+
.badges img {
|
445 |
+
height: 20px !important;
|
446 |
+
transition: transform 0.2s ease !important;
|
447 |
+
}
|
448 |
+
|
449 |
+
.badges img:hover {
|
450 |
+
transform: translateY(-2px) !important;
|
451 |
+
}
|
452 |
+
|
453 |
+
/* Remove all blockquote and code block vertical bars and backgrounds */
|
454 |
+
.markdown blockquote {
|
455 |
+
border-left: none !important;
|
456 |
+
background: none !important;
|
457 |
+
margin: 0 !important;
|
458 |
+
padding: 0 0 0 0 !important;
|
459 |
+
color: var(--white) !important;
|
460 |
+
box-shadow: none !important;
|
461 |
+
}
|
462 |
+
.markdown pre,
|
463 |
+
.markdown code {
|
464 |
+
border: none !important;
|
465 |
+
background: var(--dark-gray) !important;
|
466 |
+
box-shadow: none !important;
|
467 |
+
margin: 0 !important;
|
468 |
+
color: var(--white) !important;
|
469 |
+
}
|
470 |
+
|
471 |
+
/* Force remove any left border, box-shadow, or background from all markdown descendants */
|
472 |
+
.markdown * {
|
473 |
+
border-left: none !important;
|
474 |
+
box-shadow: none !important;
|
475 |
+
background: none !important;
|
476 |
+
color: var(--white) !important;
|
477 |
+
}
|
478 |
+
|
tools.py
CHANGED
@@ -485,7 +485,7 @@ def wikipedia_search(query: str, num_results: int = 3) -> str:
|
|
485 |
formatted_results += f"{i}. {title}\n"
|
486 |
formatted_results += f" URL: {source}\n"
|
487 |
formatted_results += f" {content}\n\n"
|
488 |
-
|
489 |
return formatted_results
|
490 |
|
491 |
except Exception as e:
|
@@ -550,7 +550,7 @@ def tavily_search(query: str, search_depth: str = "basic") -> str:
|
|
550 |
# Otherwise, just convert to string representation
|
551 |
else:
|
552 |
formatted_results += str(results)
|
553 |
-
|
554 |
return formatted_results
|
555 |
|
556 |
except Exception as e:
|
@@ -617,7 +617,9 @@ def arxiv_search(query: str, max_results: int = 5) -> str:
|
|
617 |
if len(abstract) > 300:
|
618 |
abstract = abstract[:300] + "..."
|
619 |
formatted_results += f" Abstract: {abstract}\n\n"
|
620 |
-
|
|
|
|
|
621 |
return formatted_results
|
622 |
|
623 |
except Exception as e:
|
|
|
485 |
formatted_results += f"{i}. {title}\n"
|
486 |
formatted_results += f" URL: {source}\n"
|
487 |
formatted_results += f" {content}\n\n"
|
488 |
+
print("formatted_results:", formatted_results[:100])
|
489 |
return formatted_results
|
490 |
|
491 |
except Exception as e:
|
|
|
550 |
# Otherwise, just convert to string representation
|
551 |
else:
|
552 |
formatted_results += str(results)
|
553 |
+
print("formatted_results:", formatted_results[:100])
|
554 |
return formatted_results
|
555 |
|
556 |
except Exception as e:
|
|
|
617 |
if len(abstract) > 300:
|
618 |
abstract = abstract[:300] + "..."
|
619 |
formatted_results += f" Abstract: {abstract}\n\n"
|
620 |
+
|
621 |
+
print("formatted_results:", formatted_results[:100])
|
622 |
+
|
623 |
return formatted_results
|
624 |
|
625 |
except Exception as e:
|