HFswapnil commited on
Commit
5e5edc7
·
verified ·
1 Parent(s): 494e7ba

Update src/deep_research.py

Browse files
Files changed (1) hide show
  1. src/deep_research.py +298 -295
src/deep_research.py CHANGED
@@ -1,296 +1,299 @@
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"]
 
1
+ import os
2
+ import time
3
+ import re
4
+ from re import T
5
+ import json
6
+ import operator
7
+ from typing import Annotated, Dict, Any, List, Union, Optional
8
+ from typing_extensions import TypedDict, Annotated, Literal
9
+ from dataclasses import dataclass, field
10
+
11
+ from langchain_google_genai import ChatGoogleGenerativeAI
12
+ from langchain_core.runnables import RunnableConfig
13
+ from langgraph.graph import START, END, StateGraph
14
+ from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
15
+ from tavily import TavilyClient
16
+
17
+ from model import get_gemma
18
+
19
+
20
+ gemma_model = get_gemma()
21
+
22
+ TAVILY_API_KEY = os.environ.get("TAVILY_API_KEY")
23
+
24
+ tavily_client = TavilyClient(api_key=TAVILY_API_KEY)
25
+
26
+ max_web_research_loops: int = 4
27
+
28
+
29
+ @dataclass(kw_only=True)
30
+ class SummaryState:
31
+ research_topic: str = field(default=None)
32
+ search_query: str = field(default=None)
33
+ web_search_results: Annotated[list, operator.add] = field(default_factory=list)
34
+ sources_gathered: Annotated[list, operator.add] = field(default=list)
35
+ research_loop_count: int = 0
36
+ running_summary: str = None
37
+
38
+
39
+ @dataclass(kw_only=True)
40
+ class SummaryStateInput:
41
+ research_topic: str = None
42
+
43
+ @dataclass(kw_only=True)
44
+ class SummaryStateOutput:
45
+ running_summary: str = None
46
+
47
+
48
+ # Query Writer
49
+ query_writer_instructions = """Your gola is to generate web search query.
50
+ The query will gather information about specific topic.
51
+
52
+ Topic: {research_topic}
53
+
54
+ Return your query as JSON object:
55
+ {{
56
+ "query": "string",
57
+ "aspect" : "string",
58
+ "rationale" : "string"
59
+ }}
60
+ """
61
+
62
+ # Summerizer Instructions
63
+ summerizer_instructions = """Your goal is to generate high-quality summary of the web search results.
64
+ when EXTENDING an existing summary:
65
+ 1. Seamlessly integrate new information without repeating what's already covered.
66
+ 2. Maintain consistancy with existing content's style.
67
+ 3. Only add new and non-redudant information.
68
+ 4. Ensure smooth transition between existing and new content.
69
+
70
+ when creating a NEW summary:
71
+ 1. Highlight the most relevant information from each source.
72
+ 2. Provide concise overview of the key points related to each report topic.
73
+ 3. Emphasize on significant findings or insights.
74
+ 4. Ensure coherent flow of information.
75
+
76
+ In both cases:
77
+ 1. Focus on factual & objective information
78
+ 2. Maintain consistat technical depth
79
+ 3. Avoid repetition & redundancy
80
+ 4. DON'T use phrases like "based on new results"
81
+ 5. DON'T add preamble like "Here is an extended summary ...", instead just provide summary directly
82
+ 6. DON'T add references or works cited section.
83
+ 7. You will generate tables using markdown when user asks you to do.
84
+ """
85
+
86
+ # Reflection Instructions
87
+ reflection_summary = """You are an expert research assistant analyzing summary about {research_topic}.
88
+
89
+ Your Tasks :
90
+ 1. Identify knowledge gaps or areas the need further exploration.
91
+ 2. Generate a follow-up question that would help in expanding the understanding.
92
+ 3. Focus on technical details, implementation specifics.
93
+
94
+ Ensure follow-up question is self-contained and includes necessary context for web search.
95
+
96
+ Return response as JSON object:
97
+ {{
98
+ "knowledge_gap" : "string",
99
+ "follow_up_query" : "string"
100
+ }}
101
+ """
102
+
103
+
104
+ def generate_query(state: SummaryState):
105
+ # To generate query for web search
106
+
107
+ system_message_for_query_writer = query_writer_instructions.format(research_topic=state.research_topic)
108
+
109
+ result = gemma_model.invoke(
110
+ [
111
+ HumanMessage(content=f"IMPORTANT INSTRUCTIONS:\n{system_message_for_query_writer}\n\nGenerate a query for web search")
112
+ ]
113
+ )
114
+
115
+ # print(f"[FUN] GENERATE_QUERY:\nType: {type(result)}\nContent: {result}")
116
+ raw_content = result.content.strip()
117
+ match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", raw_content, re.DOTALL)
118
+ if match:
119
+ json_str = match.group(1)
120
+ else:
121
+ raise ValueError(f"Failed to extract JSON from model response: {raw_content}")
122
+
123
+ query = json.loads(json_str)
124
+ return {"search_query" : query["query"]}
125
+
126
+
127
+ def deduplicate_and_format_sources(
128
+ search_response: Union[Dict[str, Any], List[Dict[str, Any]]],
129
+ max_tokens_per_source: int,
130
+ fetch_full_page: bool = False
131
+ ) -> str:
132
+ """
133
+ Format and deduplicate search responses from various search APIs.
134
+
135
+ Takes either a single search response or list of responses from search APIs,
136
+ deduplicates them by URL, and formats them into a structured string.
137
+
138
+ Args:
139
+ search_response (Union[Dict[str, Any], List[Dict[str, Any]]]): Either:
140
+ - A dict with a 'results' key containing a list of search results
141
+ - A list of dicts, each containing search results
142
+ max_tokens_per_source (int): Maximum number of tokens to include for each source's content
143
+ fetch_full_page (bool, optional): Whether to include the full page content. Defaults to False.
144
+
145
+ Returns:
146
+ str: Formatted string with deduplicated sources
147
+
148
+ Raises:
149
+ ValueError: If input is neither a dict with 'results' key nor a list of search results
150
+ """
151
+ # Convert input to list of results
152
+ if isinstance(search_response, dict):
153
+ sources_list = search_response['results']
154
+ elif isinstance(search_response, list):
155
+ sources_list = []
156
+ for response in search_response:
157
+ if isinstance(response, dict) and 'results' in response:
158
+ sources_list.extend(response['results'])
159
+ else:
160
+ sources_list.extend(response)
161
+ else:
162
+ raise ValueError("Input must be either a dict with 'results' or a list of search results")
163
+
164
+ # Deduplicate by URL
165
+ unique_sources = {}
166
+ for source in sources_list:
167
+ if source['url'] not in unique_sources:
168
+ unique_sources[source['url']] = source
169
+
170
+ # Format output
171
+ formatted_text = "Sources:\n\n"
172
+ for i, source in enumerate(unique_sources.values(), 1):
173
+ formatted_text += f"Source: {source['title']}\n===\n"
174
+ formatted_text += f"URL: {source['url']}\n===\n"
175
+ formatted_text += f"Most relevant content from source: {source['content']}\n===\n"
176
+ if fetch_full_page:
177
+ # Using rough estimate of 4 characters per token
178
+ char_limit = max_tokens_per_source * 4
179
+ # Handle None raw_content
180
+ raw_content = source.get('raw_content', '')
181
+ if raw_content is None:
182
+ raw_content = ''
183
+ print(f"Warning: No raw_content found for source {source['url']}")
184
+ if len(raw_content) > char_limit:
185
+ raw_content = raw_content[:char_limit] + "... [truncated]"
186
+ formatted_text += f"Full source content limited to {max_tokens_per_source} tokens: {raw_content}\n\n"
187
+
188
+ return formatted_text.strip()
189
+
190
+
191
+ def format_sources(search_results: Dict[str, Any]) -> str:
192
+ """
193
+ Format search results into a bullet-point list of sources with URLs.
194
+
195
+ Creates a simple bulleted list of search results with title and URL for each source.
196
+
197
+ Args:
198
+ search_results (Dict[str, Any]): Search response containing a 'results' key with
199
+ a list of search result objects
200
+
201
+ Returns:
202
+ str: Formatted string with sources as bullet points in the format "* title : url"
203
+ """
204
+ return '\n'.join(
205
+ f"* {source['title']} : {source['url']}"
206
+ for source in search_results['results']
207
+ )
208
+
209
+
210
+ def web_research(state: SummaryState):
211
+ search_results = tavily_client.search(state.search_query, include_raw_content=True, max_results=1)
212
+ search_str = deduplicate_and_format_sources(search_results, max_tokens_per_source=1000)
213
+ return {
214
+ "sources_gathered" : [format_sources(search_results)],
215
+ "research_loop_count" : state.research_loop_count + 1,
216
+ "web_search_results" : [search_str]
217
+ }
218
+
219
+
220
+ def summarize_sources(state: SummaryState):
221
+ existing_summary = state.running_summary
222
+ print(state.web_search_results)
223
+ most_recent_web_search = state.web_search_results[-1]
224
+
225
+ if existing_summary:
226
+ human_message = (
227
+ f"IMPORTANT INSTRUCTIONS:\n{summerizer_instructions}\n\n"
228
+ f"Extend the existing summary: {existing_summary}\n\n"
229
+ f"Include new search results: {most_recent_web_search}"
230
+ f"That addresses the following topic: {state.research_topic}"
231
+ )
232
+ else:
233
+ human_message = (
234
+ f"IMPORTANT INSTRUCTIONS:\n{summerizer_instructions}\n\n"
235
+ f"Generate summary of these search results: {most_recent_web_search}"
236
+ f"That addresses the following topic: {state.research_topic}"
237
+ )
238
+
239
+ result = gemma_model.invoke([HumanMessage(content=human_message)])
240
+
241
+ return {"running_summary" : result.content}
242
+
243
+
244
+ def reflect_on_summary(state: SummaryState):
245
+ result = gemma_model([
246
+ 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}")
247
+ ])
248
+ # print(f">> [FUN] REFLECT ON SUMMARY:\n{result.content}")
249
+ raw_content = result.content.strip()
250
+ match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", raw_content, re.DOTALL)
251
+ if match:
252
+ json_str = match.group(1)
253
+ else:
254
+ raise ValueError(f"Failed to extract JSON from model response: {raw_content}")
255
+
256
+ query = json.loads(json_str)
257
+ print(query)
258
+ return {"search_query" : query["follow_up_query"]}
259
+
260
+
261
+ def finalize_summary(state: SummaryState):
262
+ all_sources = "\n".join(source for source in state.sources_gathered)
263
+ print(f"All Sources: {all_sources}")
264
+ running_summary = f"## Summary\n\n{state.running_summary}\n\nSources:\n{all_sources}"
265
+ return {"running_summary" : running_summary}
266
+
267
+ def route_research(state: SummaryState):
268
+ if state.research_loop_count <= max_web_research_loops:
269
+ return "web_research"
270
+ else:
271
+ return "finalize_summary"
272
+
273
+
274
+ def perform_deep_research(query):
275
+
276
+ builder = StateGraph(SummaryState, input_schema=SummaryStateInput, output_schema=SummaryStateOutput)
277
+
278
+ builder.add_node("generate_query", generate_query)
279
+ builder.add_node("web_research", web_research)
280
+ builder.add_node("summarize_sources", summarize_sources)
281
+ builder.add_node("reflect_on_summary", reflect_on_summary)
282
+ builder.add_node("finalize_summary", finalize_summary)
283
+
284
+ # Add edges
285
+ builder.add_edge(START, "generate_query")
286
+ builder.add_edge("generate_query", "web_research")
287
+ builder.add_edge("web_research", "summarize_sources")
288
+ builder.add_edge("summarize_sources", "reflect_on_summary")
289
+ builder.add_conditional_edges("reflect_on_summary", route_research)
290
+ builder.add_edge("finalize_summary", END)
291
+
292
+
293
+ graph = builder.compile()
294
+
295
+ research_input = SummaryStateInput(research_topic=query)
296
+
297
+ research_output = graph.invoke(research_input)
298
+
299
  return research_output["running_summary"]