Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: Startup Due Diligence
emoji: 📈
colorFrom: blue
colorTo: indigo
sdk: gradio
sdk_version: 6.15.2
app_file: app.py
pinned: false
---

# Startup Due Diligence Agent

A multi-agent demo workflow for conducting deep research and due diligence on startup ideas, compiling findings into a structured report, and emailing it to a recipient.

🔗 **Live Demo**: [Hugging Face Space](https://huggingface.co/spaces/notaryxn/startup_due_diligence)

---

## Architecture


1. **Planner Agent** ([planner_agent.py](file:///Users/notaryxn/Desktop/Startup%20Due%20Diligence%20Agent%20/planner_agent.py)): Takes the startup idea and drafts a target web search plan (up to 5 strategic queries).
2. **Search Agent** ([search_agent.py](file:///Users/notaryxn/Desktop/Startup%20Due%20Diligence%20Agent%20/search_agent.py)): Executes the web search queries using the built-in search tool and generates concise bullet-point findings.
3. **Writer Agent** ([writer_agent.py](file:///Users/notaryxn/Desktop/Startup%20Due%20Diligence%20Agent%20/writer_agent.py)): Synthesizes the search findings into a detailed Markdown report covering market size, competitors, risks, and investment recommendations.
4. **Email Agent** ([email_agent.py](file:///Users/notaryxn/Desktop/Startup%20Due%20Diligence%20Agent%20/email_agent.py)): Converts the Markdown report into HTML and sends it via **SendGrid** to the specified recipient.
5. **Research Manager** ([research_manager.py](file:///Users/notaryxn/Desktop/Startup%20Due%20Diligence%20Agent%20/research_manager.py)): Coordinates the asynchronous execution of the agents, passing state (search results, report, and recipient email) between them.

---

## How to Run Locally

### 1. Configure Environment Variables
Create a `.env` file in the root directory:
```env
GEMINI_API_KEY=your_gemini_api_key
SENDGRID_API_KEY=your_sendgrid_api_key
```

### 2. Install Dependencies
```bash
pip install -r requirements.txt
```

### 3. Run the Gradio App
```bash
python app.py
```
This will launch the local Gradio interface in your default browser.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import os
from dotenv import load_dotenv
load_dotenv(override=True)

import gradio as gr
from research_manager import ResearchManager


async def run(query: str, recipient_email: str):
async for chunk in ResearchManager().run(query, recipient_email):
yield chunk


with gr.Blocks(theme=gr.themes.Default(primary_hue="sky")) as ui:
gr.Markdown("# Startup Due Diligence")
query_textbox = gr.Textbox(label="What idea would you like to research?")
recipient_email_textbox = gr.Textbox(label="Recipient Email Address")
run_button = gr.Button("Run", variant="primary")
report = gr.Markdown(label="Report")

run_button.click(fn=run, inputs=[query_textbox, recipient_email_textbox], outputs=report)
query_textbox.submit(fn=run, inputs=[query_textbox, recipient_email_textbox], outputs=report)

ui.launch()
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os
from dotenv import load_dotenv
load_dotenv(override=True)

import gradio as gr
from research_manager import ResearchManager


async def run(query: str, recipient_email: str):
async for chunk in ResearchManager().run(query, recipient_email):
yield chunk


with gr.Blocks(theme=gr.themes.Default(primary_hue="sky")) as ui:
gr.Markdown("# Startup Due Diligence")
query_textbox = gr.Textbox(label="What idea would you like to research?")
recipient_email_textbox = gr.Textbox(label="Recipient Email Address")
run_button = gr.Button("Run", variant="primary")
report = gr.Markdown(label="Report")

run_button.click(fn=run, inputs=[query_textbox, recipient_email_textbox], outputs=report)
query_textbox.submit(fn=run, inputs=[query_textbox, recipient_email_textbox], outputs=report)

ui.launch(inbrowser=True)


Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
from typing import Dict
from openai import AsyncOpenAI
import sendgrid
from sendgrid.helpers.mail import Email, Mail, Content, To
from agents import Agent, function_tool, OpenAIChatCompletionsModel


@function_tool
def send_email(subject: str, html_body: str, recipient_email: str) -> Dict[str, str]:
"""Send an email with the given subject, HTML body, and recipient email address"""
sg = sendgrid.SendGridAPIClient(api_key=os.environ.get("SENDGRID_API_KEY"))
from_email = Email("srivastavaaryan608@gmail.com") # put your verified sender here
to_email = To(recipient_email) # use the dynamic recipient
content = Content("text/html", html_body)
mail = Mail(from_email, to_email, subject, content).get()
response = sg.client.mail.send.post(request_body=mail)
print("Email response", response.status_code)
return "success"


INSTRUCTIONS = """You are able to send a nicely formatted HTML email based on a detailed report.
You will be provided with a detailed report and the recipient email. You should use your tool to send one email, providing the
report converted into clean, well presented HTML with an appropriate subject line, to the specified recipient email."""

gemini_model = OpenAIChatCompletionsModel(
model="gemini-2.5-flash-lite",
openai_client=AsyncOpenAI(
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
api_key=os.environ.get("GEMINI_API_KEY"),
),
)

email_agent = Agent(
name="Email agent",
instructions=INSTRUCTIONS,
tools=[send_email],
model=gemini_model,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os
from openai import AsyncOpenAI
from pydantic import BaseModel, Field
from agents import Agent, OpenAIChatCompletionsModel

HOW_MANY_SEARCHES = 5

INSTRUCTIONS = f"You are a planner agent in a startup due diligence pipeline. Given a startup idea, come up with a set of web searches \
to perform to research the market, competitors, and product viability. Output {HOW_MANY_SEARCHES} search terms."


class WebSearchItem(BaseModel):
reason: str = Field(description="Your reasoning for why this search is relevant to the due diligence.")
query: str = Field(description="The search term to use for the web search.")


class WebSearchPlan(BaseModel):
searches: list[WebSearchItem] = Field(description="A list of web searches to perform for startup due diligence.")


gemini_model = OpenAIChatCompletionsModel(
model="gemini-2.5-flash-lite",
openai_client=AsyncOpenAI(
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
api_key=os.environ.get("GEMINI_API_KEY"),
),
)

planner_agent = Agent(
name="PlannerAgent",
instructions=INSTRUCTIONS,
model=gemini_model,
output_type=WebSearchPlan,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
openai-agents
gradio
python-dotenv
sendgrid
pydantic
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from agents import Runner, trace, gen_trace_id
from search_agent import search_agent
from planner_agent import planner_agent, WebSearchItem, WebSearchPlan
from writer_agent import writer_agent, ReportData
from email_agent import email_agent
import asyncio

class ResearchManager:

async def run(self, query: str, recipient_email: str):
""" Run the deep research process, yielding the status updates and the final report"""
trace_id = gen_trace_id()
with trace("Research trace", trace_id=trace_id):
print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}")
yield f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}"
print("Starting research...")
search_plan = await self.plan_searches(query)
yield "Searches planned, starting to search..."
search_results = await self.perform_searches(search_plan)
yield "Searches complete, writing report..."
report = await self.write_report(query, search_results)
yield "Report written, sending email..."
await self.send_email(report, recipient_email)
yield "Email sent, research complete"
yield report.markdown_report


async def plan_searches(self, query: str) -> WebSearchPlan:
""" Plan the searches to perform for the query """
print("Planning searches...")
result = await Runner.run(
planner_agent,
f"Query: {query}",
)
print(f"Will perform {len(result.final_output.searches)} searches")
return result.final_output_as(WebSearchPlan)

async def perform_searches(self, search_plan: WebSearchPlan) -> list[str]:
""" Perform the searches to perform for the query """
print("Searching...")
num_completed = 0
tasks = [asyncio.create_task(self.search(item)) for item in search_plan.searches]
results = []
for task in asyncio.as_completed(tasks):
result = await task
if result is not None:
results.append(result)
num_completed += 1
print(f"Searching... {num_completed}/{len(tasks)} completed")
print("Finished searching")
return results

async def search(self, item: WebSearchItem) -> str | None:
""" Perform a search for the query """
input = f"Search term: {item.query}\nReason for searching: {item.reason}"
try:
result = await Runner.run(
search_agent,
input,
)
return str(result.final_output)
except Exception:
return None

async def write_report(self, query: str, search_results: list[str]) -> ReportData:
""" Write the report for the query """
print("Thinking about report...")
input = f"Original query: {query}\nSummarized search results: {search_results}"
result = await Runner.run(
writer_agent,
input,
)

print("Finished writing report")
return result.final_output_as(ReportData)

async def send_email(self, report: ReportData, recipient_email: str) -> None:
print("Writing email...")
result = await Runner.run(
email_agent,
f"Recipient Email: {recipient_email}\n\nReport:\n{report.markdown_report}",
)
print("Email sent")
return report
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os
from openai import AsyncOpenAI
from agents import Agent, WebSearchTool, ModelSettings, OpenAIChatCompletionsModel

INSTRUCTIONS = (
"You are a research agent in a startup due diligence pipeline. Given a search query, search the web and return a concise summary of findings.\n"
"The summary must be 2-3 paragraphs and under 300 words. Capture only facts relevant to market size, growth trends, competitors, or industry dynamics. \n"
"Write tersely — no complete sentences required, no filler. This output will be consumed by an analyst agent synthesizing a due diligence report, \n"
"so precision matters more than polish.Do not analyze, interpret, or recommend. Only report what you find. Do not include any commentary, preamble,\n"
"or closing remarks outside the summary itself."
)

gemini_model = OpenAIChatCompletionsModel(
model="gemini-2.5-flash-lite",
openai_client=AsyncOpenAI(
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
api_key=os.environ.get("GEMINI_API_KEY"),
),
)

search_agent = Agent(
name="Search agent",
instructions=INSTRUCTIONS,
tools=[WebSearchTool(search_context_size="low")],
model=gemini_model,
model_settings=ModelSettings(tool_choice="required"),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import os
from openai import AsyncOpenAI
from pydantic import BaseModel, Field
from agents import Agent, OpenAIChatCompletionsModel

INSTRUCTIONS = (
"You are a senior analyst in a startup due diligence pipeline. "
"You will be provided with the original startup idea and raw research findings from a research agent.\n"
"First, outline the structure of the report. Then write the full report based on that outline.\n"
"The report must be in markdown format, detailed and well-structured. "
"Cover market opportunity, competitive landscape, product viability, key risks, and investment recommendation. "
"Aim for 1000-1500 words minimum."
)


class ReportData(BaseModel):
short_summary: str = Field(description="A 2-3 sentence executive summary of the due diligence findings.")

markdown_report: str = Field(description="The full due diligence report in markdown.")

follow_up_questions: list[str] = Field(description="Key questions an investor should investigate further.")


gemini_model = OpenAIChatCompletionsModel(
model="gemini-2.5-flash-lite",
openai_client=AsyncOpenAI(
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
api_key=os.environ.get("GEMINI_API_KEY"),
),
)

writer_agent = Agent(
name="WriterAgent",
instructions=INSTRUCTIONS,
model=gemini_model,
output_type=ReportData,
)