Spaces:
Sleeping
Sleeping
Lumiin0us commited on
Commit ·
d9d3b1d
1
Parent(s): 50d57e4
refactor: move API keys to environment variables, add gitignore
Browse files- .gitignore +6 -0
- app/agent/__init__.py +0 -0
- app/agent/config.py +9 -0
- app/agent/graph.py +36 -0
- app/agent/nodes/__init__.py +0 -0
- app/agent/nodes/planner.py +26 -0
- app/agent/nodes/reader.py +17 -0
- app/agent/nodes/reflection.py +25 -0
- app/agent/nodes/search.py +9 -0
- app/agent/nodes/synthesizer.py +17 -0
- app/agent/state.py +10 -0
- app/streamlit.py +25 -0
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.venv
|
| 3 |
+
__pycache__
|
| 4 |
+
*.pyc
|
| 5 |
+
*.pyo
|
| 6 |
+
.DS_Store
|
app/agent/__init__.py
ADDED
|
File without changes
|
app/agent/config.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_groq import ChatGroq
|
| 2 |
+
from tavily import TavilyClient
|
| 3 |
+
import os
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
llm = ChatGroq(model="llama-3.3-70b-versatile")
|
| 9 |
+
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
|
app/agent/graph.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langgraph.graph import StateGraph, END
|
| 2 |
+
from agent.state import ResearchState
|
| 3 |
+
from agent.nodes.reader import reader_node
|
| 4 |
+
from agent.nodes.search import search_node
|
| 5 |
+
from agent.nodes.planner import planner_node
|
| 6 |
+
from agent.nodes.synthesizer import synthesizer_node
|
| 7 |
+
from agent.nodes.reflection import reflection_node, should_continue
|
| 8 |
+
|
| 9 |
+
# Create the graph
|
| 10 |
+
graph = StateGraph(ResearchState)
|
| 11 |
+
|
| 12 |
+
# Add all nodes
|
| 13 |
+
graph.add_node("planner", planner_node)
|
| 14 |
+
graph.add_node("search", search_node)
|
| 15 |
+
graph.add_node("reader", reader_node)
|
| 16 |
+
graph.add_node("synthesizer", synthesizer_node)
|
| 17 |
+
graph.add_node("reflection", reflection_node)
|
| 18 |
+
|
| 19 |
+
# Add edges (flow between nodes)
|
| 20 |
+
graph.set_entry_point("planner")
|
| 21 |
+
graph.add_edge("planner", "search")
|
| 22 |
+
graph.add_edge("search", "reader")
|
| 23 |
+
graph.add_edge("reader", "synthesizer")
|
| 24 |
+
graph.add_edge("synthesizer", "reflection")
|
| 25 |
+
|
| 26 |
+
# Conditional edge - reflection either ends or loops back
|
| 27 |
+
graph.add_conditional_edges(
|
| 28 |
+
"reflection",
|
| 29 |
+
should_continue,
|
| 30 |
+
{
|
| 31 |
+
"search": "search", # loop back
|
| 32 |
+
"END": END # finish
|
| 33 |
+
}
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
app = graph.compile()
|
app/agent/nodes/__init__.py
ADDED
|
File without changes
|
app/agent/nodes/planner.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agent.state import ResearchState
|
| 2 |
+
from agent.config import llm
|
| 3 |
+
|
| 4 |
+
def parse_questions(text: str) -> list:
|
| 5 |
+
lines = text.strip().split("\n")
|
| 6 |
+
questions = []
|
| 7 |
+
for line in lines:
|
| 8 |
+
line = line.strip()
|
| 9 |
+
if line:
|
| 10 |
+
cleaned = line.lstrip("0123456789.-) ").strip()
|
| 11 |
+
if cleaned:
|
| 12 |
+
questions.append(cleaned)
|
| 13 |
+
return questions
|
| 14 |
+
|
| 15 |
+
def planner_node(state: ResearchState):
|
| 16 |
+
prompt = f"""
|
| 17 |
+
You are a research planner. Break this topic into
|
| 18 |
+
3-4 specific search queries:
|
| 19 |
+
|
| 20 |
+
Topic: {state['topic']}
|
| 21 |
+
|
| 22 |
+
Return only a numbered list of search queries.
|
| 23 |
+
"""
|
| 24 |
+
response = llm.invoke(prompt)
|
| 25 |
+
sub_questions = parse_questions(response.content)
|
| 26 |
+
return {"sub_questions": sub_questions}
|
app/agent/nodes/reader.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
from bs4 import BeautifulSoup
|
| 3 |
+
from agent.state import ResearchState
|
| 4 |
+
|
| 5 |
+
def reader_node(state: ResearchState):
|
| 6 |
+
scraped = []
|
| 7 |
+
|
| 8 |
+
for result in state['search_results'][:3]: # top 3 only
|
| 9 |
+
try:
|
| 10 |
+
response = httpx.get(result['url'], timeout=5)
|
| 11 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
| 12 |
+
text = ' '.join(p.text for p in soup.find_all('p'))
|
| 13 |
+
scraped.append(text[:2000])
|
| 14 |
+
except:
|
| 15 |
+
continue
|
| 16 |
+
|
| 17 |
+
return {"scraped_content": scraped}
|
app/agent/nodes/reflection.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agent.state import ResearchState
|
| 2 |
+
from agent.config import llm
|
| 3 |
+
|
| 4 |
+
def reflection_node(state: ResearchState):
|
| 5 |
+
prompt = f"""
|
| 6 |
+
Original question: {state['topic']}
|
| 7 |
+
|
| 8 |
+
Generated report: {state['report']}
|
| 9 |
+
|
| 10 |
+
Does this report fully and accurately answer the
|
| 11 |
+
original question? Reply with only YES or NO,
|
| 12 |
+
then one sentence explanation.
|
| 13 |
+
"""
|
| 14 |
+
response = llm.invoke(prompt)
|
| 15 |
+
passed = response.content.strip().upper().startswith("YES")
|
| 16 |
+
return {
|
| 17 |
+
"reflection_passed": passed,
|
| 18 |
+
"loop_count": state['loop_count'] + 1
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
def should_continue(state: ResearchState):
|
| 22 |
+
if state['reflection_passed'] or state['loop_count'] >= 2:
|
| 23 |
+
return "END"
|
| 24 |
+
else:
|
| 25 |
+
return "search"
|
app/agent/nodes/search.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agent.state import ResearchState
|
| 2 |
+
from agent.config import tavily
|
| 3 |
+
|
| 4 |
+
def search_node(state: ResearchState):
|
| 5 |
+
all_results = []
|
| 6 |
+
for question in state['sub_questions']:
|
| 7 |
+
results = tavily.search(question, max_results=3)
|
| 8 |
+
all_results.extend(results['results'])
|
| 9 |
+
return {"search_results": all_results}
|
app/agent/nodes/synthesizer.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agent.state import ResearchState
|
| 2 |
+
from agent.config import llm
|
| 3 |
+
|
| 4 |
+
def synthesizer_node(state: ResearchState):
|
| 5 |
+
content = "\n\n".join(state['scraped_content'])
|
| 6 |
+
prompt = f"""
|
| 7 |
+
You are a research analyst. Using the content below,
|
| 8 |
+
write a comprehensive structured report on:
|
| 9 |
+
|
| 10 |
+
Topic: {state['topic']}
|
| 11 |
+
|
| 12 |
+
Content: {content}
|
| 13 |
+
|
| 14 |
+
Format: Introduction, Key Findings, Conclusion, Sources
|
| 15 |
+
"""
|
| 16 |
+
response = llm.invoke(prompt)
|
| 17 |
+
return {"report": response.content}
|
app/agent/state.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TypedDict, List
|
| 2 |
+
|
| 3 |
+
class ResearchState(TypedDict):
|
| 4 |
+
topic: str # user's original question
|
| 5 |
+
sub_questions: List[str] # broken down by planner node
|
| 6 |
+
search_results: List[dict] # raw results from Tavily
|
| 7 |
+
scraped_content: List[str] # full page text
|
| 8 |
+
report: str # final synthesized report
|
| 9 |
+
reflection_passed: bool # did reflection approve?
|
| 10 |
+
loop_count: int # prevent infinite loops
|
app/streamlit.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from agent.graph import app
|
| 3 |
+
|
| 4 |
+
st.title("AutoScholar")
|
| 5 |
+
st.caption("Autonomous AI research agent — enter a topic and get a structured report")
|
| 6 |
+
|
| 7 |
+
topic = st.text_input("Enter research topic:")
|
| 8 |
+
|
| 9 |
+
if st.button("Research"):
|
| 10 |
+
if not topic.strip():
|
| 11 |
+
st.warning("Please enter a topic first")
|
| 12 |
+
else:
|
| 13 |
+
with st.spinner("Agent is researching... this may take a minute"):
|
| 14 |
+
result = app.invoke({
|
| 15 |
+
"topic": topic,
|
| 16 |
+
"sub_questions": [],
|
| 17 |
+
"search_results": [],
|
| 18 |
+
"scraped_content": [],
|
| 19 |
+
"report": "",
|
| 20 |
+
"reflection_passed": False,
|
| 21 |
+
"loop_count": 0
|
| 22 |
+
})
|
| 23 |
+
|
| 24 |
+
st.success("Research complete")
|
| 25 |
+
st.markdown(result['report'])
|