Spaces:
Sleeping
Sleeping
Email now includes proof.lean and research_log.md attachments
Browse files- proof.lean: the Lean 4 code file
- research_log.md: full research log with all tool calls, search results,
Lean code attempts, and error messages
- Agent captures detailed log of every tool call and result
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- agent.py +15 -1
- app.py +2 -0
- email_sender.py +41 -28
agent.py
CHANGED
|
@@ -170,13 +170,20 @@ async def run_agent(question: str):
|
|
| 170 |
)
|
| 171 |
cost_tracker = CostTracker()
|
| 172 |
status_log: list[str] = []
|
|
|
|
| 173 |
|
| 174 |
def add_status(msg: str):
|
| 175 |
status_log.append(msg)
|
| 176 |
|
|
|
|
|
|
|
|
|
|
| 177 |
def render_status():
|
| 178 |
return "\n".join(f"- {s}" for s in status_log)
|
| 179 |
|
|
|
|
|
|
|
|
|
|
| 180 |
messages: list[dict] = [
|
| 181 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 182 |
{"role": "user", "content": question},
|
|
@@ -228,6 +235,8 @@ async def run_agent(question: str):
|
|
| 228 |
messages.append(msg_dict)
|
| 229 |
|
| 230 |
if not assistant_msg.tool_calls:
|
|
|
|
|
|
|
| 231 |
add_status("Thinking...")
|
| 232 |
yield render_status(), None
|
| 233 |
continue
|
|
@@ -239,15 +248,20 @@ async def run_agent(question: str):
|
|
| 239 |
except json.JSONDecodeError:
|
| 240 |
fn_args = {}
|
| 241 |
|
|
|
|
|
|
|
| 242 |
result = await _handle_tool_call(
|
| 243 |
fn_name, fn_args, add_status, render_status,
|
| 244 |
-
# Pass the yield-like callback for streaming status updates
|
| 245 |
)
|
| 246 |
|
|
|
|
|
|
|
| 247 |
# final_answer terminates the loop
|
| 248 |
if fn_name == "final_answer":
|
| 249 |
add_status("Research complete!")
|
| 250 |
yield render_status(), fn_args
|
|
|
|
|
|
|
| 251 |
return
|
| 252 |
|
| 253 |
yield render_status(), None
|
|
|
|
| 170 |
)
|
| 171 |
cost_tracker = CostTracker()
|
| 172 |
status_log: list[str] = []
|
| 173 |
+
full_log: list[str] = [] # Detailed log with tool contents
|
| 174 |
|
| 175 |
def add_status(msg: str):
|
| 176 |
status_log.append(msg)
|
| 177 |
|
| 178 |
+
def log_detail(msg: str):
|
| 179 |
+
full_log.append(msg)
|
| 180 |
+
|
| 181 |
def render_status():
|
| 182 |
return "\n".join(f"- {s}" for s in status_log)
|
| 183 |
|
| 184 |
+
def get_full_log():
|
| 185 |
+
return "\n\n".join(full_log)
|
| 186 |
+
|
| 187 |
messages: list[dict] = [
|
| 188 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 189 |
{"role": "user", "content": question},
|
|
|
|
| 235 |
messages.append(msg_dict)
|
| 236 |
|
| 237 |
if not assistant_msg.tool_calls:
|
| 238 |
+
if assistant_msg.content:
|
| 239 |
+
log_detail(f"## Agent thinking\n{assistant_msg.content}")
|
| 240 |
add_status("Thinking...")
|
| 241 |
yield render_status(), None
|
| 242 |
continue
|
|
|
|
| 248 |
except json.JSONDecodeError:
|
| 249 |
fn_args = {}
|
| 250 |
|
| 251 |
+
log_detail(f"## Tool call: `{fn_name}`\n**Args:** ```json\n{json.dumps(fn_args, indent=2)[:2000]}\n```")
|
| 252 |
+
|
| 253 |
result = await _handle_tool_call(
|
| 254 |
fn_name, fn_args, add_status, render_status,
|
|
|
|
| 255 |
)
|
| 256 |
|
| 257 |
+
log_detail(f"**Result:** ```\n{result[:5000]}\n```")
|
| 258 |
+
|
| 259 |
# final_answer terminates the loop
|
| 260 |
if fn_name == "final_answer":
|
| 261 |
add_status("Research complete!")
|
| 262 |
yield render_status(), fn_args
|
| 263 |
+
# Attach full_log to result for email
|
| 264 |
+
fn_args["_full_log"] = get_full_log()
|
| 265 |
return
|
| 266 |
|
| 267 |
yield render_status(), None
|
app.py
CHANGED
|
@@ -42,10 +42,12 @@ async def respond(message, history, email):
|
|
| 42 |
answer = final_result.get("answer", "") if final_result else ""
|
| 43 |
lean_code = final_result.get("lean_code", "") if final_result else ""
|
| 44 |
verified = final_result.get("verified", False) if final_result else False
|
|
|
|
| 45 |
sent = send_result_email(
|
| 46 |
to_email=email.strip(),
|
| 47 |
question=message,
|
| 48 |
status_log=status_text,
|
|
|
|
| 49 |
answer=answer,
|
| 50 |
lean_code=lean_code,
|
| 51 |
verified=verified,
|
|
|
|
| 42 |
answer = final_result.get("answer", "") if final_result else ""
|
| 43 |
lean_code = final_result.get("lean_code", "") if final_result else ""
|
| 44 |
verified = final_result.get("verified", False) if final_result else False
|
| 45 |
+
full_log = final_result.get("_full_log", "") if final_result else ""
|
| 46 |
sent = send_result_email(
|
| 47 |
to_email=email.strip(),
|
| 48 |
question=message,
|
| 49 |
status_log=status_text,
|
| 50 |
+
full_log=full_log,
|
| 51 |
answer=answer,
|
| 52 |
lean_code=lean_code,
|
| 53 |
verified=verified,
|
email_sender.py
CHANGED
|
@@ -4,6 +4,8 @@ import os
|
|
| 4 |
import smtplib
|
| 5 |
from email.mime.text import MIMEText
|
| 6 |
from email.mime.multipart import MIMEMultipart
|
|
|
|
|
|
|
| 7 |
|
| 8 |
SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com")
|
| 9 |
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
|
|
@@ -15,11 +17,12 @@ def send_result_email(
|
|
| 15 |
to_email: str,
|
| 16 |
question: str,
|
| 17 |
status_log: str,
|
|
|
|
| 18 |
answer: str,
|
| 19 |
lean_code: str,
|
| 20 |
verified: bool,
|
| 21 |
) -> bool:
|
| 22 |
-
"""Send research results via email. Returns True on success."""
|
| 23 |
if not SMTP_EMAIL or not SMTP_PASSWORD:
|
| 24 |
print("SMTP not configured, skipping email")
|
| 25 |
return False
|
|
@@ -27,7 +30,12 @@ def send_result_email(
|
|
| 27 |
badge = "VERIFIED" if verified else "UNVERIFIED (best effort)"
|
| 28 |
subject = f"VeriDeepResearch: {question[:60]}{'...' if len(question) > 60 else ''}"
|
| 29 |
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
html = f"""\
|
| 32 |
<html>
|
| 33 |
<body style="font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px;">
|
|
@@ -49,7 +57,7 @@ def send_result_email(
|
|
| 49 |
</pre>
|
| 50 |
|
| 51 |
<details>
|
| 52 |
-
<summary><strong>
|
| 53 |
<pre style="background: #f0f0f0; padding: 12px; border-radius: 8px; font-size: 12px; white-space: pre-wrap;">
|
| 54 |
{_escape(status_log)}
|
| 55 |
</pre>
|
|
@@ -57,37 +65,42 @@ def send_result_email(
|
|
| 57 |
|
| 58 |
<hr>
|
| 59 |
<p style="color: #666; font-size: 12px;">
|
|
|
|
| 60 |
Generated by <a href="https://vilin97-verideepresearch.hf.space">VeriDeepResearch</a>.
|
| 61 |
All Lean code checked with Axle (Lean 4.28.0 + Mathlib).
|
| 62 |
</p>
|
| 63 |
</body>
|
| 64 |
</html>
|
| 65 |
"""
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
try:
|
| 93 |
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
|
|
|
|
| 4 |
import smtplib
|
| 5 |
from email.mime.text import MIMEText
|
| 6 |
from email.mime.multipart import MIMEMultipart
|
| 7 |
+
from email.mime.base import MIMEBase
|
| 8 |
+
from email import encoders
|
| 9 |
|
| 10 |
SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com")
|
| 11 |
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
|
|
|
|
| 17 |
to_email: str,
|
| 18 |
question: str,
|
| 19 |
status_log: str,
|
| 20 |
+
full_log: str,
|
| 21 |
answer: str,
|
| 22 |
lean_code: str,
|
| 23 |
verified: bool,
|
| 24 |
) -> bool:
|
| 25 |
+
"""Send research results via email with attached files. Returns True on success."""
|
| 26 |
if not SMTP_EMAIL or not SMTP_PASSWORD:
|
| 27 |
print("SMTP not configured, skipping email")
|
| 28 |
return False
|
|
|
|
| 30 |
badge = "VERIFIED" if verified else "UNVERIFIED (best effort)"
|
| 31 |
subject = f"VeriDeepResearch: {question[:60]}{'...' if len(question) > 60 else ''}"
|
| 32 |
|
| 33 |
+
msg = MIMEMultipart("mixed")
|
| 34 |
+
msg["Subject"] = subject
|
| 35 |
+
msg["From"] = f"VeriDeepResearch <{SMTP_EMAIL}>"
|
| 36 |
+
msg["To"] = to_email
|
| 37 |
+
|
| 38 |
+
# HTML body (concise summary)
|
| 39 |
html = f"""\
|
| 40 |
<html>
|
| 41 |
<body style="font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px;">
|
|
|
|
| 57 |
</pre>
|
| 58 |
|
| 59 |
<details>
|
| 60 |
+
<summary><strong>Status Log</strong></summary>
|
| 61 |
<pre style="background: #f0f0f0; padding: 12px; border-radius: 8px; font-size: 12px; white-space: pre-wrap;">
|
| 62 |
{_escape(status_log)}
|
| 63 |
</pre>
|
|
|
|
| 65 |
|
| 66 |
<hr>
|
| 67 |
<p style="color: #666; font-size: 12px;">
|
| 68 |
+
Full research log and Lean code attached as files.<br>
|
| 69 |
Generated by <a href="https://vilin97-verideepresearch.hf.space">VeriDeepResearch</a>.
|
| 70 |
All Lean code checked with Axle (Lean 4.28.0 + Mathlib).
|
| 71 |
</p>
|
| 72 |
</body>
|
| 73 |
</html>
|
| 74 |
"""
|
| 75 |
+
body = MIMEMultipart("alternative")
|
| 76 |
+
body.attach(MIMEText(f"Question: {question}\nStatus: {badge}\n\nAnswer:\n{answer}\n\nSee attached files for Lean code and full research log.", "plain"))
|
| 77 |
+
body.attach(MIMEText(html, "html"))
|
| 78 |
+
msg.attach(body)
|
| 79 |
+
|
| 80 |
+
# Attachment 1: Lean code (.lean file)
|
| 81 |
+
if lean_code.strip():
|
| 82 |
+
lean_attachment = MIMEBase("application", "octet-stream")
|
| 83 |
+
lean_attachment.set_payload(lean_code.encode("utf-8"))
|
| 84 |
+
encoders.encode_base64(lean_attachment)
|
| 85 |
+
lean_attachment.add_header(
|
| 86 |
+
"Content-Disposition",
|
| 87 |
+
"attachment",
|
| 88 |
+
filename="proof.lean",
|
| 89 |
+
)
|
| 90 |
+
msg.attach(lean_attachment)
|
| 91 |
+
|
| 92 |
+
# Attachment 2: Full research log (.md file)
|
| 93 |
+
if full_log.strip():
|
| 94 |
+
log_attachment = MIMEBase("application", "octet-stream")
|
| 95 |
+
log_content = f"# VeriDeepResearch Full Log\n\n**Question:** {question}\n**Status:** {badge}\n\n---\n\n{full_log}"
|
| 96 |
+
log_attachment.set_payload(log_content.encode("utf-8"))
|
| 97 |
+
encoders.encode_base64(log_attachment)
|
| 98 |
+
log_attachment.add_header(
|
| 99 |
+
"Content-Disposition",
|
| 100 |
+
"attachment",
|
| 101 |
+
filename="research_log.md",
|
| 102 |
+
)
|
| 103 |
+
msg.attach(log_attachment)
|
| 104 |
|
| 105 |
try:
|
| 106 |
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
|