evalforge-runtime 0.2.2__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/PKG-INFO +1 -1
  2. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/pyproject.toml +1 -1
  3. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/connectors/gmail.py +87 -43
  4. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/.github/workflows/workflow.yml +0 -0
  5. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/.gitignore +0 -0
  6. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/LICENSE +0 -0
  7. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/README.md +0 -0
  8. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/docs/decisions/001-file-forwarding-in-process-chains.md +0 -0
  9. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/fixtures/evalforge.config.yaml +0 -0
  10. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/__init__.py +0 -0
  11. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/__main__.py +0 -0
  12. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/__init__.py +0 -0
  13. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/base.py +0 -0
  14. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/__init__.py +0 -0
  15. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/email_forward.py +0 -0
  16. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/email_mark_read.py +0 -0
  17. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/email_move.py +0 -0
  18. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/email_reply.py +0 -0
  19. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/email_send.py +0 -0
  20. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/file_save_output.py +0 -0
  21. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/gdrive_upload.py +0 -0
  22. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/process_call.py +0 -0
  23. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/sharepoint_upload.py +0 -0
  24. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/builtins/webhook_post.py +0 -0
  25. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/actions/runner.py +0 -0
  26. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/auth.py +0 -0
  27. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/condition.py +0 -0
  28. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/config.py +0 -0
  29. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/connectors/__init__.py +0 -0
  30. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/connectors/base.py +0 -0
  31. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/connectors/exchange.py +0 -0
  32. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/connectors/slack.py +0 -0
  33. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/connectors/webhook.py +0 -0
  34. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/db.py +0 -0
  35. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/executor.py +0 -0
  36. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/files.py +0 -0
  37. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/observability.py +0 -0
  38. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/pipeline.py +0 -0
  39. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/scheduler.py +0 -0
  40. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/server.py +0 -0
  41. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/storage.py +0 -0
  42. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/types.py +0 -0
  43. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/src/evalforge_runtime/ui.py +0 -0
  44. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/__init__.py +0 -0
  45. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/conftest.py +0 -0
  46. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/test_config.py +0 -0
  47. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/test_connectors.py +0 -0
  48. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/test_db.py +0 -0
  49. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/test_executor.py +0 -0
  50. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/test_langfuse.py +0 -0
  51. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/test_pipeline.py +0 -0
  52. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/test_reviews.py +0 -0
  53. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/test_server.py +0 -0
  54. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/tests/test_ui.py +0 -0
  55. {evalforge_runtime-0.2.2 → evalforge_runtime-0.2.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evalforge-runtime
3
- Version: 0.2.2
3
+ Version: 0.2.5
4
4
  Summary: Runtime engine for EvalForge generated applications
5
5
  Project-URL: Homepage, https://github.com/JannisConen/evalforge-runtime
6
6
  Project-URL: Repository, https://github.com/JannisConen/evalforge-runtime
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "evalforge-runtime"
7
- version = "0.2.2"
7
+ version = "0.2.5"
8
8
  description = "Runtime engine for EvalForge generated applications"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -220,8 +220,12 @@ class GmailConnector(Connector):
220
220
  headers = {"Authorization": f"Bearer {token}"}
221
221
 
222
222
  # Add date filter to Gmail search query
223
+ # Normalize filter value: "unread" → "is:unread" for Gmail API search syntax
224
+ api_filter = self._filter
225
+ if api_filter in ("unread", "all"):
226
+ api_filter = "is:unread" if api_filter == "unread" else ""
223
227
  since_date = (datetime.now(timezone.utc) - timedelta(minutes=self._max_age_minutes)).strftime("%Y/%m/%d")
224
- query = f"{self._filter} after:{since_date}"
228
+ query = f"{api_filter} after:{since_date}".strip()
225
229
  logger.debug("Gmail API query: %s", query)
226
230
 
227
231
  async with httpx.AsyncClient() as client:
@@ -459,10 +463,82 @@ class GmailConnector(Connector):
459
463
  else:
460
464
  await self._forward_api(message_id, to, body)
461
465
 
466
+ def _build_forward_message(
467
+ self, original: email_lib.message.Message, to: str, fwd_subject: str, comment: str = ""
468
+ ) -> email.mime.text.MIMEText | email.mime.multipart.MIMEMultipart:
469
+ """Build a forward message with inline-quoted body and original attachments re-attached."""
470
+ import email.mime.multipart
471
+ import email.mime.base
472
+
473
+ html_body = self._build_forward_html(original, comment)
474
+
475
+ # Collect attachments from the original
476
+ attachments = []
477
+ if original.is_multipart():
478
+ for part in original.walk():
479
+ cd = str(part.get("Content-Disposition", ""))
480
+ if "attachment" in cd:
481
+ attachments.append(part)
482
+
483
+ if attachments:
484
+ fwd = email.mime.multipart.MIMEMultipart()
485
+ fwd["Subject"] = fwd_subject
486
+ fwd["To"] = to
487
+ fwd.attach(email.mime.text.MIMEText(html_body, "html", "utf-8"))
488
+ for part in attachments:
489
+ fwd.attach(part)
490
+ else:
491
+ fwd = email.mime.text.MIMEText(html_body, "html", "utf-8")
492
+ fwd["Subject"] = fwd_subject
493
+ fwd["To"] = to
494
+
495
+ return fwd
496
+
497
+ def _build_forward_html(
498
+ self, original: email_lib.message.Message, comment: str = ""
499
+ ) -> str:
500
+ """Build an inline-quoted HTML body for forwarding, like Gmail/Outlook do."""
501
+ from_addr = str(original.get("From", ""))
502
+ date = str(original.get("Date", ""))
503
+ subject = str(original.get("Subject", ""))
504
+ to_addr = str(original.get("To", ""))
505
+
506
+ # Extract plain-text or HTML body from original
507
+ orig_body = ""
508
+ if original.is_multipart():
509
+ for part in original.walk():
510
+ if part.get_content_type() == "text/html":
511
+ payload = part.get_payload(decode=True)
512
+ if payload:
513
+ orig_body = payload.decode(part.get_content_charset() or "utf-8", errors="replace")
514
+ break
515
+ if part.get_content_type() == "text/plain" and not orig_body:
516
+ payload = part.get_payload(decode=True)
517
+ if payload:
518
+ text = payload.decode(part.get_content_charset() or "utf-8", errors="replace")
519
+ orig_body = f"<pre>{text}</pre>"
520
+ else:
521
+ payload = original.get_payload(decode=True)
522
+ if payload:
523
+ text = payload.decode(original.get_content_charset() or "utf-8", errors="replace")
524
+ orig_body = f"<pre>{text}</pre>" if original.get_content_type() == "text/plain" else text
525
+
526
+ quoted = (
527
+ f'<div style="border-left:2px solid #ccc;padding-left:8px;margin-top:16px;color:#555">'
528
+ f'<p><b>---------- Forwarded message ----------</b><br>'
529
+ f'<b>From:</b> {from_addr}<br>'
530
+ f'<b>Date:</b> {date}<br>'
531
+ f'<b>Subject:</b> {subject}<br>'
532
+ f'<b>To:</b> {to_addr}</p>'
533
+ f'{orig_body}'
534
+ f'</div>'
535
+ )
536
+ return (comment + "<br><br>" if comment else "") + quoted
537
+
462
538
  async def _forward_smtp(self, message_id: str, to: str, body: str) -> None:
463
- """Forward via SMTP — fetches original via IMAP and re-sends."""
539
+ """Forward via SMTP — fetches original via IMAP and inlines content."""
464
540
  def _do_forward() -> None:
465
- mail = imaplib.IMAP4_SSL("imap.gmail.com")
541
+ mail = imaplib.IMAP4_SSL("imap.gmail.com", timeout=30)
466
542
  mail.login(self._mailbox, self.secrets["GMAIL_APP_PASSWORD"])
467
543
  mail.select("INBOX")
468
544
 
@@ -473,22 +549,10 @@ class GmailConnector(Connector):
473
549
  original = email_lib.message_from_bytes(msg_data[0][1])
474
550
  subject = str(original.get("Subject", ""))
475
551
  fwd_subject = subject if subject.lower().startswith("fwd:") else f"Fwd: {subject}"
476
-
477
- import email.mime.multipart
478
- fwd = email.mime.multipart.MIMEMultipart()
552
+ fwd = self._build_forward_message(original, to, fwd_subject, body)
479
553
  fwd["From"] = self._mailbox
480
- fwd["To"] = to
481
- fwd["Subject"] = fwd_subject
482
-
483
- if body:
484
- fwd.attach(email.mime.text.MIMEText(body, "html"))
485
-
486
- # Attach original as message/rfc822
487
- import email.mime.message
488
- attached = email.mime.message.MIMEMessage(original)
489
- fwd.attach(attached)
490
554
 
491
- with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
555
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465, timeout=30) as server:
492
556
  server.login(self._mailbox, self.secrets["GMAIL_APP_PASSWORD"])
493
557
  server.send_message(fwd)
494
558
  logger.info("Message %s forwarded via SMTP to %s", message_id, to)
@@ -497,26 +561,11 @@ class GmailConnector(Connector):
497
561
  await asyncio.to_thread(_do_forward)
498
562
 
499
563
  async def _forward_api(self, message_id: str, to: str, body: str) -> None:
500
- """Forward via Gmail API — fetches raw message and re-sends."""
564
+ """Forward via Gmail API — fetches original and inlines content."""
501
565
  token = await self._acquire_token()
502
566
 
503
567
  async with httpx.AsyncClient() as client:
504
- # Fetch original metadata for subject
505
- resp = await client.get(
506
- f"https://gmail.googleapis.com/gmail/v1/users/{self._mailbox}"
507
- f"/messages/{message_id}?format=metadata",
508
- headers={"Authorization": f"Bearer {token}"},
509
- )
510
- resp.raise_for_status()
511
- msg = resp.json()
512
- headers_map = {
513
- h["name"].lower(): h["value"]
514
- for h in msg.get("payload", {}).get("headers", [])
515
- }
516
- subject = headers_map.get("subject", "")
517
- fwd_subject = subject if subject.lower().startswith("fwd:") else f"Fwd: {subject}"
518
-
519
- # Fetch raw RFC822
568
+ # Fetch raw RFC822 for full original content
520
569
  raw_resp = await client.get(
521
570
  f"https://gmail.googleapis.com/gmail/v1/users/{self._mailbox}"
522
571
  f"/messages/{message_id}?format=raw",
@@ -526,15 +575,10 @@ class GmailConnector(Connector):
526
575
  raw_bytes = base64.urlsafe_b64decode(raw_resp.json().get("raw", ""))
527
576
 
528
577
  original = email_lib.message_from_bytes(raw_bytes)
529
-
530
- import email.mime.multipart
531
- import email.mime.message
532
- fwd = email.mime.multipart.MIMEMultipart()
533
- fwd["to"] = to
534
- fwd["subject"] = fwd_subject
535
- if body:
536
- fwd.attach(email.mime.text.MIMEText(body, "html"))
537
- fwd.attach(email.mime.message.MIMEMessage(original))
578
+ subject = str(original.get("Subject", ""))
579
+ fwd_subject = subject if subject.lower().startswith("fwd:") else f"Fwd: {subject}"
580
+ fwd = self._build_forward_message(original, to, fwd_subject, body)
581
+ fwd["from"] = self._mailbox
538
582
 
539
583
  raw_fwd = base64.urlsafe_b64encode(fwd.as_bytes()).decode("utf-8")
540
584