| 1 | # main.py |
| 2 | from dotenv import load_dotenv |
| 3 | load_dotenv() |
| 4 | |
| 5 | import os |
| 6 | import asyncio |
| 7 | from threading import Thread |
| 8 | import time |
| 9 | |
| 10 | import ngrok |
| 11 | from flask import Flask, request, Response |
| 12 | |
| 13 | from agentmail import AgentMail |
| 14 | from agentmail_toolkit.openai import AgentMailToolkit |
| 15 | from agents import WebSearchTool, Agent, Runner |
| 16 | |
| 17 | port = 8080 |
| 18 | domain = os.getenv("WEBHOOK_DOMAIN") |
| 19 | inbox_username = os.getenv("INBOX_USERNAME") |
| 20 | inbox = f"{inbox_username}@agentmail.to" |
| 21 | |
| 22 | target_github_repo = os.getenv("TARGET_GITHUB_REPO") |
| 23 | if not target_github_repo: |
| 24 | print("\nWARNING: The TARGET_GITHUB_REPO environment variable is not set.") |
| 25 | print("The agent will not have a specific GitHub repository to focus on.") |
| 26 | print("Please set it in your .env file (e.g., TARGET_GITHUB_REPO='owner/repository_name')\n") |
| 27 | |
| 28 | demo_target_email = os.getenv("DEMO_TARGET_EMAIL") |
| 29 | if not demo_target_email: |
| 30 | print("\nWARNING: The DEMO_TARGET_EMAIL environment variable is not set.") |
| 31 | print("The agent will not have a specific email to send the 'top starrer' outreach to.") |
| 32 | print("Please set it in your .env file (e.g., DEMO_TARGET_EMAIL='your.email@example.com')\n") |
| 33 | |
| 34 | # Determine the target email, with a fallback if the environment variable is not set. |
| 35 | |
| 36 | # The fallback is less ideal for a real demo but prevents the agent from having no target. |
| 37 | |
| 38 | actual_demo_target_email = demo_target_email if demo_target_email else "fallback.email@example.com" |
| 39 | |
| 40 | # Use a fallback for target_github_repo as well for the instructions string construction |
| 41 | |
| 42 | actual_target_github_repo = target_github_repo if target_github_repo else "example/repo" |
| 43 | |
| 44 | # --- AgentMail and Web Server Setup --- |
| 45 | |
| 46 | # 1. Initialize the AgentMail client |
| 47 | |
| 48 | client = AgentMail() |
| 49 | |
| 50 | # 2. Idempotently create the inbox for the agent |
| 51 | |
| 52 | # Using a deterministic client_id ensures we don't create duplicate inboxes |
| 53 | |
| 54 | # if the script is run multiple times. |
| 55 | |
| 56 | inbox_client_id = f"inbox-for-{inbox_username}" |
| 57 | print(f"Attempting to create or retrieve inbox '{inbox}' with client_id: {inbox_client_id}") |
| 58 | try: |
| 59 | client.inboxes.create( |
| 60 | username=inbox_username, |
| 61 | client_id=inbox_client_id |
| 62 | ) |
| 63 | print("Inbox creation/retrieval successful.") |
| 64 | except Exception as e: |
| 65 | print(f"Error creating/retrieving inbox: {e}") # Depending on the desired behavior, you might want to exit here # if the inbox is critical for the agent's function. |
| 66 | |
| 67 | # 3. Start the ngrok tunnel to get a public URL |
| 68 | |
| 69 | print("Starting ngrok tunnel...") |
| 70 | listener = ngrok.forward(port, domain=domain, authtoken_from_env=True) |
| 71 | print(f"ngrok tunnel started: {listener.url()}") |
| 72 | |
| 73 | # 4. Idempotently create the webhook pointing to our new public URL |
| 74 | |
| 75 | webhook_url = f"{listener.url()}/webhooks" |
| 76 | webhook_client_id = f"webhook-for-{inbox_username}" |
| 77 | print(f"Attempting to create or retrieve webhook for URL: {webhook_url}") |
| 78 | try: |
| 79 | client.webhooks.create( |
| 80 | url=webhook_url, |
| 81 | client_id=webhook_client_id, |
| 82 | ) |
| 83 | print("Webhook creation/retrieval successful.") |
| 84 | except Exception as e: |
| 85 | print(f"Error creating/retrieving webhook: {e}") |
| 86 | |
| 87 | # 5. Initialize the Flask App |
| 88 | |
| 89 | app = Flask(**name**) |
| 90 | |
| 91 | instructions = f""" |
| 92 | You are a GitHub Repository Evangelist Agent. Your name is AgentMail. Your email address is {inbox}. |
| 93 | Your primary focus is the GitHub repository: '{actual_target_github_repo}'. |
| 94 | Your goal is to engage the user at {actual_demo_target_email} about the potential of '{actual_target_github_repo}' for building AI agents, using rich HTML emails. |
| 95 | |
| 96 | **You operate in two main scenarios:** |
| 97 | |
| 98 | **Scenario 1: Proactive Outreach (Triggered by internal monitor for '{actual_target_github_repo}')** |
| 99 | |
| 100 | - You will receive a direct instruction when a new (simulated) star is detected for '{actual_target_github_repo}'. |
| 101 | - This instruction will explicitly ask you to: |
| 102 | 1. Use the WebSearchTool to find fresh, compelling information or talking points about '{actual_target_github_repo}' (e.g., new features, use cases for agent development, benefits). You should synthesize this information, not just copy it. |
| 103 | 2. Use the 'send_message' tool to send a NEW email to {actual_demo_target_email}. |
| 104 | - The email should start by mentioning something like: "Hello! We noticed you recently showed interest in (or starred) our repository, '{actual_target_github_repo}'! We're excited to share some insights..." |
| 105 | - You must craft an engaging 'subject' for this email. |
| 106 | - You must craft an informative 'html' (body) for this email in HTML format, based on your synthesized web search findings. **Do NOT include raw URLs or direct links from your web search in the email body.** Instead, discuss the concepts or information you found. |
| 107 | - The email must end with a clear call to action inviting the user to ask you (the agent) questions. For example: "I'm an AI assistant for '{actual_target_github_repo}', ready to answer any questions you might have. Feel free to reply to this email with your thoughts or queries!" |
| 108 | - Your final output for THIS SCENARIO (after the send_message tool call) should be a brief confirmation message (e.g., "Proactive HTML email about new star sent to {actual_demo_target_email}."). Do NOT output the email content itself as your final response here, as the tool handles sending it. |
| 109 | |
| 110 | **Scenario 2: Replying to Emails (Triggered by webhook when an email arrives at {inbox})** |
| 111 | |
| 112 | - You will receive the content of an incoming email (From, Subject, Body). |
| 113 | - **If the email is FROM '{actual_demo_target_email}':** |
| 114 | - This is a reply from your primary contact. Your goal is to continue the conversation naturally and persuasively. **Your entire output for this interaction MUST be a single, well-formed HTML string for the email body. It must start directly with an HTML tag (e.g., `<p>`) and end with a closing HTML tag. Do NOT include any other text, labels, comments, or markdown-style code fences (like `html ... ` or '''html: ...''') before or after the HTML content itself.** |
| 115 | - Use the WebSearchTool to find relevant new information about '{actual_target_github_repo}' to answer their questions, address their points, or further highlight the repository's value for agent development. |
| 116 | - **Strict Conciseness for Guides/Steps:** If the user asks for instructions, a guide, or steps (e.g., "how to install", "integration guide", "how to use X feature"), your reply MUST be **extremely concise (max 2-3 sentences summarizing the core idea)** and provide **ONE primary HTML hyperlink** to the most relevant page in the official documentation (e.g., `https://docs.agentstack.sh`). **Absolutely do NOT list multiple steps, commands, or code snippets in the email for these types of requests.** Your goal is to direct them to the documentation for details. |
| 117 | - **HTML Formatting for All Replies:** |
| 118 | - Use `<p>` tags for paragraphs. Avoid empty `<p></p>` tags or excessive `<br />` tags to prevent unwanted spacing. |
| 119 | - For emphasis, use `<strong>` or `<em>`. |
| 120 | - If, for a question _not_ about general guides/steps, a short code snippet is essential for a direct answer, you MUST wrap it in `<pre><code>...code...</code></pre>` tags. But avoid this for guide-type questions. |
| 121 | - All URLs you intend to be clickable MUST be formatted as HTML hyperlinks: `<a href="URL\">Clickable Link Text</a>`. Do not output raw URLs or markdown-style links. |
| 122 | - For example, a reply to "how to install" MUST be similar to: `<p>You can install AgentStack using package managers like Homebrew or pipx. For the detailed commands and options, please consult our official <a href='https://docs.agentstack.sh/installation'>Installation Guide</a>.</p><p>Is there anything else specific I can help you find in the docs or a different question perhaps?</p>` |
| 123 | - The webhook handler will use your **raw string output** directly as the HTML body of the reply. |
| 124 | - **If the email is FROM ANY OTHER ADDRESS:** |
| 125 | - This is unexpected. Politely state (in simple HTML, using one or two `<p>` tags, **and no surrounding fences or labels**) that you are an automated agent focused on discussing '{actual_target_github_repo}' with {actual_demo_target_email} and cannot assist with other requests at this time. |
| 126 | - Your output for this interaction should be ONLY this polite, **raw HTML email body.** |
| 127 | |
| 128 | **General Guidelines for HTML Emails to {actual_demo_target_email}:** |
| 129 | _ Always be enthusiastic and informative about '{actual_target_github_repo}'. |
| 130 | _ Tailor your points based on information you find with the WebSearchTool. For initial outreach, synthesize information. **For replies asking for guides/steps, BE EXTREMELY CONCISE, summarize in 2-3 sentences, and provide a single link to the main documentation.** |
| 131 | _ Initial outreach: concise (5-6 sentences). Replies answering specific, non-guide questions: aim for 7-10 sentences. **Replies to guide/installation/integration questions: MAX 4 sentences, including the link.** |
| 132 | _ Structure ALL content with appropriate HTML tags: `<p>`, `<br />` (sparingly), `<strong>`, `<em>`, `<u>`, `<ul>`, `<ol>`, `<li>` (if not a guide question), `<pre><code>` (if not a guide question and essential), and **`<a href="URL">link text</a>` for ALL clickable links.** NO MARKDOWN-STYLE LINKS. |
| 133 | |
| 134 | - \*\*IMPORTANT: Your output for replies (Scenario 2, when email is from {actual*demo_target_email}) MUST be \_only* the HTML content itself. Do not wrap it in markdown code fences (like ```html), or any other prefix/suffix text.** Start directly with `<p>` or another HTML tag. |
| 135 | - Encourage interaction. The initial email must end with an invitation to reply with questions. \* Maintain conversation context. |
| 136 | |
| 137 | Remember, your primary contact for ongoing conversation is '{actual_demo_target_email}', and your primary topic is always '{actual_target_github_repo}'. |
| 138 | """ |
| 139 | |
| 140 | agent = Agent( |
| 141 | name="GitHub Agent", |
| 142 | instructions=instructions, |
| 143 | tools=AgentMailToolkit(client).get_tools() + [WebSearchTool()], |
| 144 | ) |
| 145 | |
| 146 | messages = [] |
| 147 | |
| 148 | # --- GitHub Polling Logic --- |
| 149 | |
| 150 | simulated_stargazer_count = 0 |
| 151 | MAX_SIMULATED_STARS = 1 # single star even |
| 152 | stars_found_so_far = 0 |
| 153 | |
| 154 | def poll_github_stargazers(): |
| 155 | global simulated_stargazer_count, stars_found_so_far |
| 156 | print(f"GitHub polling thread started for top 20 repositories related to AI agents...") |
| 157 | |
| 158 | # Give the Flask app a moment to start up if run concurrently |
| 159 | time.sleep(3) |
| 160 | |
| 161 | while stars_found_so_far < MAX_SIMULATED_STARS: |
| 162 | time.sleep(13) # Poll every 30 seconds for the demo |
| 163 | |
| 164 | # Simulate a new star appearing |
| 165 | new_star_detected = False |
| 166 | # For demo, let's just add a star each time for the first few polls |
| 167 | if stars_found_so_far < MAX_SIMULATED_STARS: # Check again inside loop |
| 168 | simulated_stargazer_count += 1 |
| 169 | stars_found_so_far += 1 |
| 170 | new_star_detected = True |
| 171 | print(f"[POLLER] New star! Total: {simulated_stargazer_count}") |
| 172 | |
| 173 | if new_star_detected and actual_target_github_repo != "example/repo" and actual_demo_target_email != "fallback.email@example.com": |
| 174 | prompt_for_agent = f"""\ |
| 175 | URGENT TASK: A new star has been detected for the repository '{actual_target_github_repo}' (simulated count: {simulated_stargazer_count}). |
| 176 | Your goal is to use the 'send_message' tool to notify {actual_demo_target_email} with an HTML email that does not contain direct web links in its body and has a specific call to action. |
| 177 | |
| 178 | Thought: I need to perform two steps: first, gather information using WebSearchTool, and second, synthesize this information into an HTML email and send it using the send_message tool. |
| 179 | |
| 180 | Step 1: Gather Information. |
| 181 | Use the WebSearchTool to find ONE fresh, compelling piece of information or talking point about '{actual_target_github_repo}' relevant to AI agent development. |
| 182 | Your output for this step should be an action call to WebSearchTool. For example: |
| 183 | Action: WebSearchTool("key features of {actual_target_github_repo} for AI agents") |
| 184 | |
| 185 | (After you receive the observation from WebSearchTool, you will proceed to Step 2 in your next turn) |
| 186 | |
| 187 | Step 2: Formulate and Send HTML Email. |
| 188 | Based on the information from WebSearchTool, you MUST call the 'send_message' tool. |
| 189 | The email should start by acknowledging the user's interest, e.g., "<p>Hello! We noticed you recently showed interest in (or starred) our repository, <strong>{actual_target_github_repo}</strong>! We're excited to share some insights...</p>" |
| 190 | The email body should discuss the information you found but **MUST NOT include any raw URLs or direct hyperlinks from the web search results.** Synthesize the information. |
| 191 | The email MUST end with a call to action like: "<p>I'm an AI assistant for '{actual_target_github_repo}', and I'm here to help answer any questions you might have. Feel free to reply to this email with your thoughts or if there's anything specific you'd like to know!</p>" |
| 192 | |
| 193 | The parameters for the 'send_message' tool call should be: |
| 194 | - 'to': ['{actual_demo_target_email}'] |
| 195 | - 'inbox_id': '{inbox}' |
| 196 | - 'subject': An engaging subject based on the web search findings (e.g., "Insights on {actual_target_github_repo} for Your AI Projects!"). |
| 197 | - 'html': An email body in HTML format, adhering to all the above content and formatting rules (mention star, no direct links, specific CTA). |
| 198 | |
| 199 | Your output for this step MUST be an action call to 'send_message' with the tool input formatted as a valid JSON string, ensuring you use the 'html' field for the body. For example: |
| 200 | Action: send_message(``` |
| 201 | {{ |
| 202 | "inbox_id": "{inbox}", |
| 203 | "to": ["{actual_demo_target_email}"], |
| 204 | "subject": "Following Up on Your Interest in {actual_target_github_repo}!", |
| 205 | "html": "<p>Hello! We noticed you recently showed interest in <strong>{actual_target_github_repo}</strong>!</p><p>We've been developing some exciting capabilities within it, particularly around [synthesized information from web search, e.g., its new modular design for agent development]. This allows for more flexible integration of AI components.</p><p>I'm an AI assistant for \'{actual_target_github_repo}\', and I\'m here to help answer any questions you might have. Feel free to reply to this email with your thoughts or if there\'s anything specific you\'d like to know!</p>" |
| 206 | }} |
| 207 | ```) |
| 208 | |
| 209 | If you cannot find information with WebSearchTool in Step 1, for Step 2 you should still attempt to call send_message. The HTML email should still acknowledge the star and provide the specified CTA, but state that fresh specific updates couldn't be retrieved at this moment, while still highlighting the general value of '{actual_target_github_repo}'. |
| 210 | Your final conversational output after the 'send_message' action is executed by the system should be a simple confirmation like "Email dispatch initiated to {actual_demo_target_email}." |
| 211 | """ |
| 212 | print(f"[POLLER] Triggering agent for new star on {actual_target_github_repo} to notify {actual_demo_target_email}") |
| 213 | # We run the agent in a blocking way here for simplicity in the polling thread. |
| 214 | # The 'messages' history is intentionally kept separate from the webhook's conversation history for this proactive outreach. |
| 215 | try: |
| 216 | response = asyncio.run(Runner.run(agent, [{"role": "user", "content": prompt_for_agent}])) |
| 217 | print(f"[POLLER] Agent response to new star prompt: {response.final_output}") |
| 218 | # You could add a more specific check here if the agent is supposed to return a structured success/failure |
| 219 | if "email dispatch initiated" not in response.final_output.lower(): |
| 220 | print(f"[POLLER_WARNING] Agent response did not explicitly confirm email sending according to expected pattern: {response.final_output}") |
| 221 | except Exception as e: |
| 222 | print(f"[POLLER_ERROR] An error occurred while the agent was processing the new star prompt: {e}") |
| 223 | import traceback # Import traceback here to use it |
| 224 | print(f"[POLLER_ERROR] Traceback: {traceback.format_exc()}") |
| 225 | elif new_star_detected: |
| 226 | print("[POLLER] Simulated new star, but TARGET_GITHUB_REPO or DEMO_TARGET_EMAIL is not properly set. Skipping agent trigger.") |
| 227 | |
| 228 | @app.route("/webhooks", methods=["POST"]) |
| 229 | def receive_webhook(): |
| 230 | print(f"\n[/webhooks] Received webhook. Payload keys: {list(request.json.keys()) if request.is_json else 'Not JSON or empty'}") |
| 231 | Thread(target=process_webhook, args=(request.json,)).start() |
| 232 | return Response(status=200) |
| 233 | |
| 234 | def process_webhook(payload): |
| 235 | global messages |
| 236 | |
| 237 | email = payload["message"] |
| 238 | print(f"[process_webhook] Processing email from: {email.get('from')}, subject: {email.get('subject')}, id: {email.get('message_id')}") |
| 239 | |
| 240 | prompt = f""" |
| 241 | From: {email["from"]} |
| 242 | Subject: {email["subject"]} |
| 243 | Body:\n{email["text"]} |
| 244 | """ |
| 245 | print("Prompt:\n\n", prompt, "\n") |
| 246 | |
| 247 | response = asyncio.run(Runner.run(agent, messages + [{"role": "user", "content": prompt}])) |
| 248 | print("Response:\n\n", response.final_output, "\n") |
| 249 | |
| 250 | print(f"[process_webhook] Attempting to send reply to message_id: {email['message_id']} via inbox: {inbox}") |
| 251 | client.inboxes.messages.reply(inbox_id=inbox, message_id=email["message_id"], html=response.final_output) |
| 252 | print(f"[process_webhook] Reply call made for message_id: {email['message_id']}.") |
| 253 | |
| 254 | messages = response.to_input_list() |
| 255 | print(f"[process_webhook] Updated message history. New length: {len(messages)}\n") |
| 256 | |
| 257 | if **name** == "**main**": |
| 258 | print(f"Inbox: {inbox}\n") |
| 259 | if not target_github_repo or target_github_repo == "example/repo": |
| 260 | print("WARNING: TARGET_GITHUB_REPO not set or is default. Poller will not be effective.") |
| 261 | if not demo_target_email: |
| 262 | print("WARNING: DEMO_TARGET_EMAIL not set or is default. Poller will not be effective.") |
| 263 | |
| 264 | polling_thread = Thread(target=poll_github_stargazers) |
| 265 | polling_thread.daemon = True # So it exits when the main thread exits |
| 266 | polling_thread.start() |
| 267 | |
| 268 | print(f"ngrok tunnel started: {listener.url()}") |
| 269 | |
| 270 | app.run(port=port) |