Lumiin0us commited on
Commit
d9d3b1d
·
1 Parent(s): 50d57e4

refactor: move API keys to environment variables, add gitignore

Browse files
.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'])