Liorlsa9 commited on
Commit
736beb3
·
1 Parent(s): d8634b7

Add initial implementation of deep research agents and email functionality

Browse files

- Created app.py for user interface with Gradio, allowing users to input research queries and receive reports via email.
- Implemented deep_research_agents including:
- email_agent.py for sending formatted HTML emails using SendGrid.
- planner_agent.py for generating search plans based on user queries.
- research_manager.py to orchestrate the research process, including planning searches, performing them, writing reports, and sending emails.
- search_agent.py for summarizing search results.
- writer_agent.py for generating detailed reports from research findings.
- Integrated dotenv for environment variable management.
- Added support for dynamic email recipients in research reports.
- Established a cohesive flow for research tasks and email notifications.

app.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from dotenv import load_dotenv
3
+ from deep_research_agent.research_manager import ResearchManager
4
+ import sendgrid
5
+ from sendgrid.helpers.mail import Email, Mail, Content, To
6
+ import os
7
+ import ssl
8
+ ssl._create_default_https_context = ssl._create_unverified_context
9
+
10
+ load_dotenv(override=True)
11
+
12
+ # Custom email sender to use the user's email
13
+ async def send_email_to_user(subject: str, html_body: str, user_email: str):
14
+ sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY'))
15
+ from_email = Email("liorsolomon.solid@gmail.com") # put your verified sender here
16
+ to_email = To(user_email)
17
+ content = Content("text/html", html_body)
18
+ mail = Mail(from_email, to_email, subject, content).get()
19
+ response = sg.client.mail.send.post(request_body=mail)
20
+ print("Email response", response.status_code)
21
+ return {"status": "success"}
22
+
23
+ # Patch the ResearchManager to allow dynamic recipient email
24
+ def make_research_manager_with_email(user_email):
25
+ class PatchedResearchManager(ResearchManager):
26
+ async def send_email(self, report):
27
+ subject = "Your Deep Research Report"
28
+ html_body = report.markdown_report.replace('\n', '<br>')
29
+ await send_email_to_user(subject, html_body, user_email)
30
+ return report
31
+ return PatchedResearchManager()
32
+
33
+ async def run_research(query: str, user_email: str):
34
+ manager = make_research_manager_with_email(user_email)
35
+ yield "<b>🔍 Research started! Please wait while we process your request...</b>"
36
+ async for chunk in manager.run(query):
37
+ # Show step-by-step progress to the user
38
+ if chunk.startswith("View trace: "):
39
+ yield f"<i>Trace available: <a href='{chunk[11:]}' target='_blank'>{chunk[11:]}</a></i>"
40
+ elif chunk.lower().startswith("searches planned"):
41
+ yield "<b>📝 Searches planned. Starting to search the web...</b>"
42
+ elif chunk.lower().startswith("searches complete"):
43
+ yield "<b>✅ Searches complete. Writing the report...</b>"
44
+ elif chunk.lower().startswith("report written"):
45
+ yield "<b>🖊️ Report written. Sending the report to your email...</b>"
46
+ elif chunk.lower().startswith("email sent"):
47
+ yield "<b>📧 Email sent! Research complete. See your inbox for the full report.</b>"
48
+ elif chunk.strip().startswith('#'):
49
+ yield chunk
50
+ else:
51
+ yield f"<i>{chunk}</i>"
52
+
53
+ with gr.Blocks(theme=gr.themes.Default(primary_hue="sky")) as ui:
54
+ gr.Markdown("# Deep Research Chatbot\nEnter your email and research topic. You'll receive a detailed report by email!")
55
+ with gr.Row():
56
+ email_box = gr.Textbox(label="Your Email", type="text", placeholder="your@email.com")
57
+ query_box = gr.Textbox(label="Research Query", lines=3, placeholder="What would you like to research?")
58
+ run_btn = gr.Button("Run Deep Research", variant="primary")
59
+ report_md = gr.Markdown(label="Report Preview (sent to your email)")
60
+
61
+ run_btn.click(run_research, inputs=[query_box, email_box], outputs=report_md)
62
+ query_box.submit(run_research, inputs=[query_box, email_box], outputs=report_md)
63
+ email_box.submit(run_research, inputs=[query_box, email_box], outputs=report_md)
64
+
65
+ if __name__ == "__main__":
66
+ ui.launch(inbrowser=True)
deep_research_agents/deep_research.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from dotenv import load_dotenv
3
+ from .research_manager import ResearchManager
4
+
5
+ load_dotenv(override=True)
6
+
7
+
8
+ async def run(query: str):
9
+ async for chunk in ResearchManager().run(query):
10
+ yield chunk
11
+
12
+
13
+ with gr.Blocks(theme=gr.themes.Default(primary_hue="sky")) as ui:
14
+ gr.Markdown("# Deep Research")
15
+ query_textbox = gr.Textbox(label="What topic would you like to research?")
16
+ run_button = gr.Button("Run", variant="primary")
17
+ report = gr.Markdown(label="Report")
18
+
19
+ run_button.click(fn=run, inputs=query_textbox, outputs=report)
20
+ query_textbox.submit(fn=run, inputs=query_textbox, outputs=report)
21
+
22
+ ui.launch(inbrowser=True)
23
+
deep_research_agents/email_agent.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Dict
3
+
4
+ import sendgrid
5
+ from sendgrid.helpers.mail import Email, Mail, Content, To
6
+ from agents import Agent, function_tool
7
+ import markdown2
8
+
9
+ @function_tool
10
+ def send_email(subject: str, html_body: str) -> Dict[str, str]:
11
+ """ Send an email with the given subject and HTML body (markdown supported) """
12
+ sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY'))
13
+ from_email = Email("liorsolomon.solid@gmail.com") # put your verified sender here
14
+ to_email = To("liorsolomon.solid@gmail.com") # put your recipient here
15
+ # Convert markdown to HTML and wrap in a basic template
16
+ html_content = markdown2.markdown(html_body)
17
+ html_template = f"""
18
+ <html>
19
+ <head>
20
+ <style>
21
+ body {{ font-family: Arial, sans-serif; margin: 2em; }}
22
+ h1, h2, h3 {{ color: #0074D9; }}
23
+ p {{ line-height: 1.6; }}
24
+ </style>
25
+ </head>
26
+ <body>
27
+ {html_content}
28
+ </body>
29
+ </html>
30
+ """
31
+ content = Content("text/html", html_template)
32
+ mail = Mail(from_email, to_email, subject, content).get()
33
+ response = sg.client.mail.send.post(request_body=mail)
34
+ print("Email response", response.status_code)
35
+ return {"status": "success"}
36
+
37
+ INSTRUCTIONS = """You are able to send a nicely formatted HTML email based on a detailed report.
38
+ You will be provided with a detailed report. You should use your tool to send one email, providing the
39
+ report converted into clean, well presented HTML with an appropriate subject line."""
40
+
41
+ email_agent = Agent(
42
+ name="Email agent",
43
+ instructions=INSTRUCTIONS,
44
+ tools=[send_email],
45
+ model="gpt-4o-mini",
46
+ )
deep_research_agents/planner_agent.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from agents import Agent
3
+
4
+ HOW_MANY_SEARCHES = 5
5
+
6
+ INSTRUCTIONS = f"You are a helpful research assistant. Given a query, come up with a set of web searches \
7
+ to perform to best answer the query. Output {HOW_MANY_SEARCHES} terms to query for."
8
+
9
+
10
+ class WebSearchItem(BaseModel):
11
+ reason: str = Field(description="Your reasoning for why this search is important to the query.")
12
+ query: str = Field(description="The search term to use for the web search.")
13
+
14
+
15
+ class WebSearchPlan(BaseModel):
16
+ searches: list[WebSearchItem] = Field(description="A list of web searches to perform to best answer the query.")
17
+
18
+ planner_agent = Agent(
19
+ name="PlannerAgent",
20
+ instructions=INSTRUCTIONS,
21
+ model="gpt-4o-mini",
22
+ output_type=WebSearchPlan,
23
+ )
deep_research_agents/research_manager.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agents import Runner, trace, gen_trace_id
2
+ from .search_agent import search_agent
3
+ from .planner_agent import planner_agent, WebSearchItem, WebSearchPlan
4
+ from .writer_agent import writer_agent, ReportData
5
+ from .email_agent import email_agent
6
+ import asyncio
7
+
8
+ class ResearchManager:
9
+
10
+ async def run(self, query: str):
11
+ """ Run the deep research process, yielding the status updates and the final report"""
12
+ trace_id = gen_trace_id()
13
+ with trace("Research trace", trace_id=trace_id):
14
+ print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}")
15
+ yield f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}"
16
+ print("Starting research...")
17
+ search_plan = await self.plan_searches(query)
18
+ yield "Searches planned, starting to search..."
19
+ search_results = await self.perform_searches(search_plan)
20
+ yield "Searches complete, writing report..."
21
+ report = await self.write_report(query, search_results)
22
+ yield "Report written, sending email..."
23
+ await self.send_email(report)
24
+ yield "Email sent, research complete"
25
+ yield report.markdown_report
26
+
27
+
28
+ async def plan_searches(self, query: str) -> WebSearchPlan:
29
+ """ Plan the searches to perform for the query """
30
+ print("Planning searches...")
31
+ result = await Runner.run(
32
+ planner_agent,
33
+ f"Query: {query}",
34
+ )
35
+ print(f"Will perform {len(result.final_output.searches)} searches")
36
+ return result.final_output_as(WebSearchPlan)
37
+
38
+ async def perform_searches(self, search_plan: WebSearchPlan) -> list[str]:
39
+ """ Perform the searches to perform for the query """
40
+ print("Searching...")
41
+ num_completed = 0
42
+ tasks = [asyncio.create_task(self.search(item)) for item in search_plan.searches]
43
+ results = []
44
+ for task in asyncio.as_completed(tasks):
45
+ result = await task
46
+ if result is not None:
47
+ results.append(result)
48
+ num_completed += 1
49
+ print(f"Searching... {num_completed}/{len(tasks)} completed")
50
+ print("Finished searching")
51
+ return results
52
+
53
+ async def search(self, item: WebSearchItem) -> str | None:
54
+ """ Perform a search for the query """
55
+ input = f"Search term: {item.query}\nReason for searching: {item.reason}"
56
+ try:
57
+ result = await Runner.run(
58
+ search_agent,
59
+ input,
60
+ )
61
+ return str(result.final_output)
62
+ except Exception:
63
+ return None
64
+
65
+ async def write_report(self, query: str, search_results: list[str]) -> ReportData:
66
+ """ Write the report for the query """
67
+ print("Thinking about report...")
68
+ input = f"Original query: {query}\nSummarized search results: {search_results}"
69
+ result = await Runner.run(
70
+ writer_agent,
71
+ input,
72
+ )
73
+
74
+ print("Finished writing report")
75
+ return result.final_output_as(ReportData)
76
+
77
+ async def send_email(self, report: ReportData) -> None:
78
+ print("Writing email...")
79
+ result = await Runner.run(
80
+ email_agent,
81
+ report.markdown_report,
82
+ )
83
+ print("Email sent")
84
+ return report
deep_research_agents/search_agent.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agents import Agent, WebSearchTool, ModelSettings
2
+
3
+ INSTRUCTIONS = (
4
+ "You are a research assistant. Given a search term, you search the web for that term and "
5
+ "produce a concise summary of the results. The summary must 2-3 paragraphs and less than 300 "
6
+ "words. Capture the main points. Write succintly, no need to have complete sentences or good "
7
+ "grammar. This will be consumed by someone synthesizing a report, so its vital you capture the "
8
+ "essence and ignore any fluff. Do not include any additional commentary other than the summary itself."
9
+ )
10
+
11
+ search_agent = Agent(
12
+ name="Search agent",
13
+ instructions=INSTRUCTIONS,
14
+ tools=[WebSearchTool(search_context_size="low")],
15
+ model="gpt-4o-mini",
16
+ model_settings=ModelSettings(tool_choice="required"),
17
+ )
deep_research_agents/writer_agent.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from agents import Agent
3
+
4
+ INSTRUCTIONS = (
5
+ "You are a senior researcher tasked with writing a cohesive report for a research query. "
6
+ "You will be provided with the original query, and some initial research done by a research assistant.\n"
7
+ "You should first come up with an outline for the report that describes the structure and "
8
+ "flow of the report. Then, generate the report and return that as your final output.\n"
9
+ "The final output should be in markdown format, and it should be lengthy and detailed. Aim "
10
+ "for 5-10 pages of content, at least 1000 words."
11
+ )
12
+
13
+
14
+ class ReportData(BaseModel):
15
+ short_summary: str = Field(description="A short 2-3 sentence summary of the findings.")
16
+
17
+ markdown_report: str = Field(description="The final report")
18
+
19
+ follow_up_questions: list[str] = Field(description="Suggested topics to research further")
20
+
21
+
22
+ writer_agent = Agent(
23
+ name="WriterAgent",
24
+ instructions=INSTRUCTIONS,
25
+ model="gpt-4o-mini",
26
+ output_type=ReportData,
27
+ )