Upload 6 files
Browse files- src/app.py +130 -0
- src/deep_research.py +296 -0
- src/model.py +19 -0
- src/rag_utils.py +54 -0
- src/run_model.py +21 -0
- src/web_search.py +76 -0
src/app.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from run_model import generate_response, generate_RAG_response
|
| 3 |
+
from web_search import search_web
|
| 4 |
+
from deep_research import perform_deep_research
|
| 5 |
+
import tempfile
|
| 6 |
+
|
| 7 |
+
st.set_page_config(layout="wide")
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
st.title("💬 Chat with Gemma")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
with st.sidebar:
|
| 14 |
+
st.title("Tools")
|
| 15 |
+
st.markdown("For source code of this website, visit : <some_link>")
|
| 16 |
+
|
| 17 |
+
option = st.selectbox(
|
| 18 |
+
"Choose tools",
|
| 19 |
+
("Simple Chat", "Web Search", "Upload PDF", "Deep Web Search"),
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
temperature = st.slider(
|
| 23 |
+
label="Temperature (controls randomness)",
|
| 24 |
+
min_value=0.0,
|
| 25 |
+
max_value=2.0,
|
| 26 |
+
value=1.0,
|
| 27 |
+
step=0.01,
|
| 28 |
+
help="Lower = more deterministic, Higher = more random"
|
| 29 |
+
)
|
| 30 |
+
# Top-k sampling
|
| 31 |
+
top_k = st.slider(
|
| 32 |
+
label="Top-k (limits to top K tokens by probability)",
|
| 33 |
+
min_value=0,
|
| 34 |
+
max_value=100,
|
| 35 |
+
value=50,
|
| 36 |
+
step=1,
|
| 37 |
+
help="0 = disable top-k filtering"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Top-p (nucleus sampling)
|
| 41 |
+
top_p = st.slider(
|
| 42 |
+
label="Top-p (nucleus sampling cutoff)",
|
| 43 |
+
min_value=0.0,
|
| 44 |
+
max_value=1.0,
|
| 45 |
+
value=0.9,
|
| 46 |
+
step=0.01,
|
| 47 |
+
help="0.0 = conservative, 1.0 = more random"
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
if option == "Upload PDF":
|
| 51 |
+
file = st.file_uploader(label="Uploaded file will provide context to LLM", type="pdf")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if option == "Web Search":
|
| 55 |
+
st.write("Web Search Enabled for next query")
|
| 56 |
+
# WB_SEARCH = True
|
| 57 |
+
|
| 58 |
+
if option == "None":
|
| 59 |
+
st.warning("You are not using any tool")
|
| 60 |
+
|
| 61 |
+
if option == "Deep Web Search":
|
| 62 |
+
st.write("Deep Web Research Enabled for next query")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# col1, col2 = st.columns([6, 1], gap="small")
|
| 67 |
+
|
| 68 |
+
# column 1
|
| 69 |
+
# with col1:
|
| 70 |
+
if "messages" not in st.session_state:
|
| 71 |
+
st.session_state.messages = []
|
| 72 |
+
|
| 73 |
+
for msg in st.session_state.messages:
|
| 74 |
+
with st.chat_message(msg["role"]):
|
| 75 |
+
st.markdown(msg["content"])
|
| 76 |
+
|
| 77 |
+
# prompt = st.chat_input("Say something...")
|
| 78 |
+
|
| 79 |
+
if prompt:= st.chat_input("Say something..."):
|
| 80 |
+
# Display user message
|
| 81 |
+
st.chat_message("user").markdown(prompt)
|
| 82 |
+
st.session_state.messages.append({"role": "user", "content": prompt})
|
| 83 |
+
if option == "Simple Chat":
|
| 84 |
+
|
| 85 |
+
response = generate_response(history=st.session_state.messages, query=prompt, temperature=temperature, top_k=top_k, top_p=top_p)
|
| 86 |
+
st.chat_message("assistant").markdown(response)
|
| 87 |
+
# st.session_state.messages.append({"role": "assistant", "content": response})
|
| 88 |
+
|
| 89 |
+
if option == "Web Search":
|
| 90 |
+
st.session_state.messages.append([{"role": "user", "content" : prompt}])
|
| 91 |
+
|
| 92 |
+
response, sources = search_web(prompt)
|
| 93 |
+
# asnswer = response
|
| 94 |
+
# st.chat_message("assistant").markdown(f"{response}\n\n###Sources\n{'\n'.join([source for source in sources])}")
|
| 95 |
+
with st.chat_message("assistant"):
|
| 96 |
+
st.markdown(f"{response}\n\n### Sources\n" + "\n".join(sources))
|
| 97 |
+
|
| 98 |
+
if option == "Deep Web Search":
|
| 99 |
+
st.session_state.messages.append([{"role": "user", "content" : prompt}])
|
| 100 |
+
|
| 101 |
+
response = perform_deep_research(prompt)
|
| 102 |
+
# asnswer = response
|
| 103 |
+
# st.chat_message("assistant").markdown(f"{response}\n\n###Sources\n{'\n'.join([source for source in sources])}")
|
| 104 |
+
with st.chat_message("assistant"):
|
| 105 |
+
st.markdown(response)
|
| 106 |
+
|
| 107 |
+
if option == "Upload PDF":
|
| 108 |
+
st.session_state.messages.append([{"role": "user", "content" : prompt}])
|
| 109 |
+
|
| 110 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
|
| 111 |
+
tmp_file.write(file.read())
|
| 112 |
+
tmp_path = tmp_file.name
|
| 113 |
+
print(tmp_path)
|
| 114 |
+
response = generate_RAG_response(prompt, tmp_path, st.session_state.messages)
|
| 115 |
+
# print(file)
|
| 116 |
+
with st.chat_message("assistant"):
|
| 117 |
+
st.markdown(response)
|
| 118 |
+
|
| 119 |
+
st.session_state.messages.append({"role": "assistant", "content": response})
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# # Slider in column 2
|
| 123 |
+
# with col2:
|
| 124 |
+
# temperature = st.slider("Temperature", 0.0, 1.0, 0.0)
|
| 125 |
+
# top_k = st.slider("Top k", 0.0, 100.0, 40.0)
|
| 126 |
+
# top_p = st.slider("Top p", 0.0, 1.0, 0.95)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
if __name__ == "__main__":
|
| 130 |
+
main()
|
src/deep_research.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 2 |
+
import os
|
| 3 |
+
from typing import Annotated, Dict, Any, List, Union, Optional
|
| 4 |
+
from typing_extensions import TypedDict, Annotated, Literal
|
| 5 |
+
import operator
|
| 6 |
+
from dataclasses import dataclass, field
|
| 7 |
+
from re import T
|
| 8 |
+
import json
|
| 9 |
+
from langchain_core.runnables import RunnableConfig
|
| 10 |
+
from langgraph.graph import START, END, StateGraph
|
| 11 |
+
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
| 12 |
+
import time
|
| 13 |
+
from tavily import TavilyClient
|
| 14 |
+
import re
|
| 15 |
+
from model import get_gemma
|
| 16 |
+
|
| 17 |
+
from api_key import TAVILY_API_KEY
|
| 18 |
+
|
| 19 |
+
gemma_model = get_gemma()
|
| 20 |
+
|
| 21 |
+
tavily_client = TavilyClient(api_key=TAVILY_API_KEY)
|
| 22 |
+
|
| 23 |
+
max_web_research_loops: int = 4
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass(kw_only=True)
|
| 27 |
+
class SummaryState:
|
| 28 |
+
research_topic: str = field(default=None)
|
| 29 |
+
search_query: str = field(default=None)
|
| 30 |
+
web_search_results: Annotated[list, operator.add] = field(default_factory=list)
|
| 31 |
+
sources_gathered: Annotated[list, operator.add] = field(default=list)
|
| 32 |
+
research_loop_count: int = 0
|
| 33 |
+
running_summary: str = None
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass(kw_only=True)
|
| 37 |
+
class SummaryStateInput:
|
| 38 |
+
research_topic: str = None
|
| 39 |
+
|
| 40 |
+
@dataclass(kw_only=True)
|
| 41 |
+
class SummaryStateOutput:
|
| 42 |
+
running_summary: str = None
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# Query Writer
|
| 46 |
+
query_writer_instructions = """Your gola is to generate web search query.
|
| 47 |
+
The query will gather information about specific topic.
|
| 48 |
+
|
| 49 |
+
Topic: {research_topic}
|
| 50 |
+
|
| 51 |
+
Return your query as JSON object:
|
| 52 |
+
{{
|
| 53 |
+
"query": "string",
|
| 54 |
+
"aspect" : "string",
|
| 55 |
+
"rationale" : "string"
|
| 56 |
+
}}
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
# Summerizer Instructions
|
| 60 |
+
summerizer_instructions = """Your goal is to generate high-quality summary of the web search results.
|
| 61 |
+
when EXTENDING an existing summary:
|
| 62 |
+
1. Seamlessly integrate new information without repeating what's already covered.
|
| 63 |
+
2. Maintain consistancy with existing content's style.
|
| 64 |
+
3. Only add new and non-redudant information.
|
| 65 |
+
4. Ensure smooth transition between existing and new content.
|
| 66 |
+
|
| 67 |
+
when creating a NEW summary:
|
| 68 |
+
1. Highlight the most relevant information from each source.
|
| 69 |
+
2. Provide concise overview of the key points related to each report topic.
|
| 70 |
+
3. Emphasize on significant findings or insights.
|
| 71 |
+
4. Ensure coherent flow of information.
|
| 72 |
+
|
| 73 |
+
In both cases:
|
| 74 |
+
1. Focus on factual & objective information
|
| 75 |
+
2. Maintain consistat technical depth
|
| 76 |
+
3. Avoid repetition & redundancy
|
| 77 |
+
4. DON'T use phrases like "based on new results"
|
| 78 |
+
5. DON'T add preamble like "Here is an extended summary ...", instead just provide summary directly
|
| 79 |
+
6. DON'T add references or works cited section.
|
| 80 |
+
7. You will generate tables using markdown when user asks you to do.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
# Reflection Instructions
|
| 84 |
+
reflection_summary = """You are an expert research assistant analyzing summary about {research_topic}.
|
| 85 |
+
|
| 86 |
+
Your Tasks :
|
| 87 |
+
1. Identify knowledge gaps or areas the need further exploration.
|
| 88 |
+
2. Generate a follow-up question that would help in expanding the understanding.
|
| 89 |
+
3. Focus on technical details, implementation specifics.
|
| 90 |
+
|
| 91 |
+
Ensure follow-up question is self-contained and includes necessary context for web search.
|
| 92 |
+
|
| 93 |
+
Return response as JSON object:
|
| 94 |
+
{{
|
| 95 |
+
"knowledge_gap" : "string",
|
| 96 |
+
"follow_up_query" : "string"
|
| 97 |
+
}}
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def generate_query(state: SummaryState):
|
| 102 |
+
# To generate query for web search
|
| 103 |
+
|
| 104 |
+
system_message_for_query_writer = query_writer_instructions.format(research_topic=state.research_topic)
|
| 105 |
+
|
| 106 |
+
result = gemma_model.invoke(
|
| 107 |
+
[
|
| 108 |
+
HumanMessage(content=f"IMPORTANT INSTRUCTIONS:\n{system_message_for_query_writer}\n\nGenerate a query for web search")
|
| 109 |
+
]
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
# print(f"[FUN] GENERATE_QUERY:\nType: {type(result)}\nContent: {result}")
|
| 113 |
+
raw_content = result.content.strip()
|
| 114 |
+
match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", raw_content, re.DOTALL)
|
| 115 |
+
if match:
|
| 116 |
+
json_str = match.group(1)
|
| 117 |
+
else:
|
| 118 |
+
raise ValueError(f"Failed to extract JSON from model response: {raw_content}")
|
| 119 |
+
|
| 120 |
+
query = json.loads(json_str)
|
| 121 |
+
return {"search_query" : query["query"]}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def deduplicate_and_format_sources(
|
| 125 |
+
search_response: Union[Dict[str, Any], List[Dict[str, Any]]],
|
| 126 |
+
max_tokens_per_source: int,
|
| 127 |
+
fetch_full_page: bool = False
|
| 128 |
+
) -> str:
|
| 129 |
+
"""
|
| 130 |
+
Format and deduplicate search responses from various search APIs.
|
| 131 |
+
|
| 132 |
+
Takes either a single search response or list of responses from search APIs,
|
| 133 |
+
deduplicates them by URL, and formats them into a structured string.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
search_response (Union[Dict[str, Any], List[Dict[str, Any]]]): Either:
|
| 137 |
+
- A dict with a 'results' key containing a list of search results
|
| 138 |
+
- A list of dicts, each containing search results
|
| 139 |
+
max_tokens_per_source (int): Maximum number of tokens to include for each source's content
|
| 140 |
+
fetch_full_page (bool, optional): Whether to include the full page content. Defaults to False.
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
str: Formatted string with deduplicated sources
|
| 144 |
+
|
| 145 |
+
Raises:
|
| 146 |
+
ValueError: If input is neither a dict with 'results' key nor a list of search results
|
| 147 |
+
"""
|
| 148 |
+
# Convert input to list of results
|
| 149 |
+
if isinstance(search_response, dict):
|
| 150 |
+
sources_list = search_response['results']
|
| 151 |
+
elif isinstance(search_response, list):
|
| 152 |
+
sources_list = []
|
| 153 |
+
for response in search_response:
|
| 154 |
+
if isinstance(response, dict) and 'results' in response:
|
| 155 |
+
sources_list.extend(response['results'])
|
| 156 |
+
else:
|
| 157 |
+
sources_list.extend(response)
|
| 158 |
+
else:
|
| 159 |
+
raise ValueError("Input must be either a dict with 'results' or a list of search results")
|
| 160 |
+
|
| 161 |
+
# Deduplicate by URL
|
| 162 |
+
unique_sources = {}
|
| 163 |
+
for source in sources_list:
|
| 164 |
+
if source['url'] not in unique_sources:
|
| 165 |
+
unique_sources[source['url']] = source
|
| 166 |
+
|
| 167 |
+
# Format output
|
| 168 |
+
formatted_text = "Sources:\n\n"
|
| 169 |
+
for i, source in enumerate(unique_sources.values(), 1):
|
| 170 |
+
formatted_text += f"Source: {source['title']}\n===\n"
|
| 171 |
+
formatted_text += f"URL: {source['url']}\n===\n"
|
| 172 |
+
formatted_text += f"Most relevant content from source: {source['content']}\n===\n"
|
| 173 |
+
if fetch_full_page:
|
| 174 |
+
# Using rough estimate of 4 characters per token
|
| 175 |
+
char_limit = max_tokens_per_source * 4
|
| 176 |
+
# Handle None raw_content
|
| 177 |
+
raw_content = source.get('raw_content', '')
|
| 178 |
+
if raw_content is None:
|
| 179 |
+
raw_content = ''
|
| 180 |
+
print(f"Warning: No raw_content found for source {source['url']}")
|
| 181 |
+
if len(raw_content) > char_limit:
|
| 182 |
+
raw_content = raw_content[:char_limit] + "... [truncated]"
|
| 183 |
+
formatted_text += f"Full source content limited to {max_tokens_per_source} tokens: {raw_content}\n\n"
|
| 184 |
+
|
| 185 |
+
return formatted_text.strip()
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def format_sources(search_results: Dict[str, Any]) -> str:
|
| 189 |
+
"""
|
| 190 |
+
Format search results into a bullet-point list of sources with URLs.
|
| 191 |
+
|
| 192 |
+
Creates a simple bulleted list of search results with title and URL for each source.
|
| 193 |
+
|
| 194 |
+
Args:
|
| 195 |
+
search_results (Dict[str, Any]): Search response containing a 'results' key with
|
| 196 |
+
a list of search result objects
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
str: Formatted string with sources as bullet points in the format "* title : url"
|
| 200 |
+
"""
|
| 201 |
+
return '\n'.join(
|
| 202 |
+
f"* {source['title']} : {source['url']}"
|
| 203 |
+
for source in search_results['results']
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def web_research(state: SummaryState):
|
| 208 |
+
search_results = tavily_client.search(state.search_query, include_raw_content=True, max_results=1)
|
| 209 |
+
search_str = deduplicate_and_format_sources(search_results, max_tokens_per_source=1000)
|
| 210 |
+
return {
|
| 211 |
+
"sources_gathered" : [format_sources(search_results)],
|
| 212 |
+
"research_loop_count" : state.research_loop_count + 1,
|
| 213 |
+
"web_search_results" : [search_str]
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def summarize_sources(state: SummaryState):
|
| 218 |
+
existing_summary = state.running_summary
|
| 219 |
+
print(state.web_search_results)
|
| 220 |
+
most_recent_web_search = state.web_search_results[-1]
|
| 221 |
+
|
| 222 |
+
if existing_summary:
|
| 223 |
+
human_message = (
|
| 224 |
+
f"IMPORTANT INSTRUCTIONS:\n{summerizer_instructions}\n\n"
|
| 225 |
+
f"Extend the existing summary: {existing_summary}\n\n"
|
| 226 |
+
f"Include new search results: {most_recent_web_search}"
|
| 227 |
+
f"That addresses the following topic: {state.research_topic}"
|
| 228 |
+
)
|
| 229 |
+
else:
|
| 230 |
+
human_message = (
|
| 231 |
+
f"IMPORTANT INSTRUCTIONS:\n{summerizer_instructions}\n\n"
|
| 232 |
+
f"Generate summary of these search results: {most_recent_web_search}"
|
| 233 |
+
f"That addresses the following topic: {state.research_topic}"
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
result = gemma_model.invoke([HumanMessage(content=human_message)])
|
| 237 |
+
|
| 238 |
+
return {"running_summary" : result.content}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def reflect_on_summary(state: SummaryState):
|
| 242 |
+
result = gemma_model([
|
| 243 |
+
HumanMessage(content=f"IMPORTANT INSTRUCTIONS:\n{reflection_summary.format(research_topic=state.research_topic)}\n\nIdentify a knowledge gap and generate a follow-up web search query based on existing knowledge: {state.running_summary}")
|
| 244 |
+
])
|
| 245 |
+
# print(f">> [FUN] REFLECT ON SUMMARY:\n{result.content}")
|
| 246 |
+
raw_content = result.content.strip()
|
| 247 |
+
match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", raw_content, re.DOTALL)
|
| 248 |
+
if match:
|
| 249 |
+
json_str = match.group(1)
|
| 250 |
+
else:
|
| 251 |
+
raise ValueError(f"Failed to extract JSON from model response: {raw_content}")
|
| 252 |
+
|
| 253 |
+
query = json.loads(json_str)
|
| 254 |
+
print(query)
|
| 255 |
+
return {"search_query" : query["follow_up_query"]}
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def finalize_summary(state: SummaryState):
|
| 259 |
+
all_sources = "\n".join(source for source in state.sources_gathered)
|
| 260 |
+
print(f"All Sources: {all_sources}")
|
| 261 |
+
running_summary = f"## Summary\n\n{state.running_summary}\n\nSources:\n{all_sources}"
|
| 262 |
+
return {"running_summary" : running_summary}
|
| 263 |
+
|
| 264 |
+
def route_research(state: SummaryState):
|
| 265 |
+
if state.research_loop_count <= max_web_research_loops:
|
| 266 |
+
return "web_research"
|
| 267 |
+
else:
|
| 268 |
+
return "finalize_summary"
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def perform_deep_research(query):
|
| 272 |
+
|
| 273 |
+
builder = StateGraph(SummaryState, input_schema=SummaryStateInput, output_schema=SummaryStateOutput)
|
| 274 |
+
|
| 275 |
+
builder.add_node("generate_query", generate_query)
|
| 276 |
+
builder.add_node("web_research", web_research)
|
| 277 |
+
builder.add_node("summarize_sources", summarize_sources)
|
| 278 |
+
builder.add_node("reflect_on_summary", reflect_on_summary)
|
| 279 |
+
builder.add_node("finalize_summary", finalize_summary)
|
| 280 |
+
|
| 281 |
+
# Add edges
|
| 282 |
+
builder.add_edge(START, "generate_query")
|
| 283 |
+
builder.add_edge("generate_query", "web_research")
|
| 284 |
+
builder.add_edge("web_research", "summarize_sources")
|
| 285 |
+
builder.add_edge("summarize_sources", "reflect_on_summary")
|
| 286 |
+
builder.add_conditional_edges("reflect_on_summary", route_research)
|
| 287 |
+
builder.add_edge("finalize_summary", END)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
graph = builder.compile()
|
| 291 |
+
|
| 292 |
+
research_input = SummaryStateInput(research_topic=query)
|
| 293 |
+
|
| 294 |
+
research_output = graph.invoke(research_input)
|
| 295 |
+
|
| 296 |
+
return research_output["running_summary"]
|
src/model.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from api_key import GOOGLE_API_KEY
|
| 2 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def get_gemma(temperature=0, top_k=40, top_p=0.95):
|
| 7 |
+
|
| 8 |
+
gemma_model = ChatGoogleGenerativeAI(
|
| 9 |
+
model="gemma-3-12b-it",
|
| 10 |
+
temperature=temperature,
|
| 11 |
+
max_tokens=2048,
|
| 12 |
+
timeout=None,
|
| 13 |
+
max_retries=2,
|
| 14 |
+
google_api_key=GOOGLE_API_KEY,
|
| 15 |
+
top_k=top_k,
|
| 16 |
+
top_p=top_p
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
return gemma_model
|
src/rag_utils.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 2 |
+
from langchain_community.document_loaders import PyPDFLoader
|
| 3 |
+
from langchain_chroma import Chroma
|
| 4 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
|
| 8 |
+
|
| 9 |
+
def load_pdf_document(file_path):
|
| 10 |
+
document_loader = PyPDFLoader(file_path)
|
| 11 |
+
return document_loader.load()
|
| 12 |
+
|
| 13 |
+
def chunk_documents(raw_documents):
|
| 14 |
+
text_processor = RecursiveCharacterTextSplitter(
|
| 15 |
+
chunk_size = 1000,
|
| 16 |
+
chunk_overlap = 200,
|
| 17 |
+
add_start_index = True
|
| 18 |
+
)
|
| 19 |
+
return text_processor.split_documents(raw_documents)
|
| 20 |
+
|
| 21 |
+
def find_related_documents(query, vector_database):
|
| 22 |
+
# return vector_database.similarity_search(query, k=2)
|
| 23 |
+
return vector_database.max_marginal_relevance_search(query, k=2, fetch_k=5, lambda_mult=0.6)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def ProcessDocuments(document_path: str) -> str:
|
| 27 |
+
|
| 28 |
+
loaded_doc = load_pdf_document(document_path)
|
| 29 |
+
chunked_doc = chunk_documents(loaded_doc)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
vector_database = Chroma(
|
| 33 |
+
persist_directory=f"./chroma_store/{document_path.split("\\")[-1].split(".")[0]}",
|
| 34 |
+
embedding_function=embedding_model
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
vector_database.add_documents(chunked_doc)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def generate_context(query: str, file: str):
|
| 41 |
+
|
| 42 |
+
ProcessDocuments(file)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
vector_database = Chroma(
|
| 46 |
+
persist_directory=f"./chroma_store/{file.split("\\")[-1].split(".")[0]}",
|
| 47 |
+
embedding_function=embedding_model
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
relevant_docs = find_related_documents(query, vector_database)
|
| 51 |
+
context_text = "\n".join([doc.page_content for doc in relevant_docs])
|
| 52 |
+
|
| 53 |
+
return query, context_text
|
| 54 |
+
|
src/run_model.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from model import get_gemma
|
| 2 |
+
from rag_utils import generate_context
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def generate_response(history=[], temperature: float=0.0, top_k=None, top_p=None):
|
| 6 |
+
|
| 7 |
+
gemma_model = get_gemma(temperature=temperature, top_k=top_k, top_p=top_p)
|
| 8 |
+
|
| 9 |
+
response = gemma_model.invoke(history).content
|
| 10 |
+
|
| 11 |
+
return response
|
| 12 |
+
|
| 13 |
+
def generate_RAG_response(query: str, file_path, history=[]):
|
| 14 |
+
gemma_model = get_gemma()
|
| 15 |
+
query, context = generate_context(query, file_path)
|
| 16 |
+
|
| 17 |
+
history[-1] = {"role" : "user", "content" : f"INSTRUCTION: Answer the query with given context in mind.\nQUERY: {query}\n\nCONTEXT : {context}"}
|
| 18 |
+
|
| 19 |
+
response = gemma_model.invoke(history).content
|
| 20 |
+
|
| 21 |
+
return response
|
src/web_search.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.agents import AgentExecutor, create_react_agent
|
| 2 |
+
from langchain.agents import load_tools, Tool
|
| 3 |
+
# from langchain.tools import DuckDuckGoSearchResults
|
| 4 |
+
from tavily import TavilyClient
|
| 5 |
+
from langchain.prompts import PromptTemplate
|
| 6 |
+
from model import get_gemma
|
| 7 |
+
from api_key import TAVILY_API_KEY
|
| 8 |
+
|
| 9 |
+
gemma_model = get_gemma()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Create the ReAct template
|
| 13 |
+
react_template = """Answer the following questions as best you can. You have access to the following tools:
|
| 14 |
+
|
| 15 |
+
{tools}
|
| 16 |
+
|
| 17 |
+
Use the following format:
|
| 18 |
+
|
| 19 |
+
Question: the input question you must answer
|
| 20 |
+
Thought: you should always think about what to do
|
| 21 |
+
Action: the action to take, should be one of [{tool_names}]
|
| 22 |
+
Action Input: the input to the action
|
| 23 |
+
Observation: the result of the action
|
| 24 |
+
... (this Thought/Action/Action Input/Observation can repeat N times)
|
| 25 |
+
Thought: I now know the final answer
|
| 26 |
+
Final Answer: the final answer to the original input question
|
| 27 |
+
|
| 28 |
+
Begin!
|
| 29 |
+
|
| 30 |
+
Question: {input}
|
| 31 |
+
Thought:{agent_scratchpad}"""
|
| 32 |
+
|
| 33 |
+
prompt = PromptTemplate(
|
| 34 |
+
template=react_template,
|
| 35 |
+
input_variables=["tools", "tool_names", "input", "agent_scratchpad"]
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
tavily_client = TavilyClient(api_key=TAVILY_API_KEY)
|
| 40 |
+
|
| 41 |
+
tavily_search_tool = Tool(
|
| 42 |
+
name="tavily search",
|
| 43 |
+
description = "A web search engine. Use this to as a search engine for general queries.",
|
| 44 |
+
func = lambda x: tavily_client.search(x, max_results=1)
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# Prepare tools
|
| 48 |
+
tools = load_tools(["llm-math"], llm=gemma_model)
|
| 49 |
+
tools.append(tavily_search_tool)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# Construct the ReAct agent
|
| 53 |
+
agent = create_react_agent(gemma_model, tools, prompt)
|
| 54 |
+
agent_executor = AgentExecutor(
|
| 55 |
+
agent=agent,
|
| 56 |
+
tools=tools,
|
| 57 |
+
verbose=True,
|
| 58 |
+
handle_parsing_errors=True,
|
| 59 |
+
return_intermediate_steps=True
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def get_urls_from_response(response):
|
| 63 |
+
urls = []
|
| 64 |
+
for step in response["intermediate_steps"]:
|
| 65 |
+
urls.append(step[1]["results"][0]["url"])
|
| 66 |
+
return urls
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def search_web(query):
|
| 70 |
+
response = agent_executor.invoke({"input" : query})
|
| 71 |
+
|
| 72 |
+
output = response["output"]
|
| 73 |
+
sources = get_urls_from_response(response)
|
| 74 |
+
|
| 75 |
+
return output, sources
|
| 76 |
+
|