evalforge-runtime 0.2.1__tar.gz → 0.2.2__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.1 → evalforge_runtime-0.2.2}/PKG-INFO +1 -1
  2. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/pyproject.toml +1 -1
  3. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/connectors/gmail.py +100 -0
  4. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/.github/workflows/workflow.yml +0 -0
  5. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/.gitignore +0 -0
  6. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/LICENSE +0 -0
  7. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/README.md +0 -0
  8. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/docs/decisions/001-file-forwarding-in-process-chains.md +0 -0
  9. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/fixtures/evalforge.config.yaml +0 -0
  10. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/__init__.py +0 -0
  11. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/__main__.py +0 -0
  12. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/__init__.py +0 -0
  13. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/base.py +0 -0
  14. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/__init__.py +0 -0
  15. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/email_forward.py +0 -0
  16. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/email_mark_read.py +0 -0
  17. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/email_move.py +0 -0
  18. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/email_reply.py +0 -0
  19. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/email_send.py +0 -0
  20. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/file_save_output.py +0 -0
  21. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/gdrive_upload.py +0 -0
  22. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/process_call.py +0 -0
  23. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/sharepoint_upload.py +0 -0
  24. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/builtins/webhook_post.py +0 -0
  25. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/actions/runner.py +0 -0
  26. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/auth.py +0 -0
  27. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/condition.py +0 -0
  28. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/config.py +0 -0
  29. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/connectors/__init__.py +0 -0
  30. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/connectors/base.py +0 -0
  31. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/connectors/exchange.py +0 -0
  32. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/connectors/slack.py +0 -0
  33. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/connectors/webhook.py +0 -0
  34. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/db.py +0 -0
  35. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/executor.py +0 -0
  36. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/files.py +0 -0
  37. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/observability.py +0 -0
  38. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/pipeline.py +0 -0
  39. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/scheduler.py +0 -0
  40. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/server.py +0 -0
  41. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/storage.py +0 -0
  42. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/types.py +0 -0
  43. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/src/evalforge_runtime/ui.py +0 -0
  44. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/__init__.py +0 -0
  45. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/conftest.py +0 -0
  46. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/test_config.py +0 -0
  47. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/test_connectors.py +0 -0
  48. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/test_db.py +0 -0
  49. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/test_executor.py +0 -0
  50. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/test_langfuse.py +0 -0
  51. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/test_pipeline.py +0 -0
  52. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/test_reviews.py +0 -0
  53. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/test_server.py +0 -0
  54. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/tests/test_ui.py +0 -0
  55. {evalforge_runtime-0.2.1 → evalforge_runtime-0.2.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evalforge-runtime
3
- Version: 0.2.1
3
+ Version: 0.2.2
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.1"
7
+ version = "0.2.2"
8
8
  description = "Runtime engine for EvalForge generated applications"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -447,6 +447,106 @@ class GmailConnector(Connector):
447
447
  resp.raise_for_status()
448
448
  logger.info("Reply sent via Gmail API (thread=%s)", thread_id)
449
449
 
450
+ # -----------------------------------------------------------------------
451
+ # Forward
452
+ # -----------------------------------------------------------------------
453
+
454
+ async def forward(self, message_id: str, to: str, body: str = "") -> None:
455
+ """Forward a message to another recipient."""
456
+ logger.info("Forwarding message %s to %s", message_id, to)
457
+ if self._auth_method != "service_account":
458
+ await self._forward_smtp(message_id, to, body)
459
+ else:
460
+ await self._forward_api(message_id, to, body)
461
+
462
+ async def _forward_smtp(self, message_id: str, to: str, body: str) -> None:
463
+ """Forward via SMTP — fetches original via IMAP and re-sends."""
464
+ def _do_forward() -> None:
465
+ mail = imaplib.IMAP4_SSL("imap.gmail.com")
466
+ mail.login(self._mailbox, self.secrets["GMAIL_APP_PASSWORD"])
467
+ mail.select("INBOX")
468
+
469
+ _, msg_data = mail.fetch(message_id.encode(), "(RFC822)")
470
+ if not msg_data or not msg_data[0] or not isinstance(msg_data[0], tuple):
471
+ raise ValueError(f"Could not fetch original message {message_id}")
472
+
473
+ original = email_lib.message_from_bytes(msg_data[0][1])
474
+ subject = str(original.get("Subject", ""))
475
+ fwd_subject = subject if subject.lower().startswith("fwd:") else f"Fwd: {subject}"
476
+
477
+ import email.mime.multipart
478
+ fwd = email.mime.multipart.MIMEMultipart()
479
+ 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
+
491
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
492
+ server.login(self._mailbox, self.secrets["GMAIL_APP_PASSWORD"])
493
+ server.send_message(fwd)
494
+ logger.info("Message %s forwarded via SMTP to %s", message_id, to)
495
+ mail.logout()
496
+
497
+ await asyncio.to_thread(_do_forward)
498
+
499
+ async def _forward_api(self, message_id: str, to: str, body: str) -> None:
500
+ """Forward via Gmail API — fetches raw message and re-sends."""
501
+ token = await self._acquire_token()
502
+
503
+ 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
520
+ raw_resp = await client.get(
521
+ f"https://gmail.googleapis.com/gmail/v1/users/{self._mailbox}"
522
+ f"/messages/{message_id}?format=raw",
523
+ headers={"Authorization": f"Bearer {token}"},
524
+ )
525
+ raw_resp.raise_for_status()
526
+ raw_bytes = base64.urlsafe_b64decode(raw_resp.json().get("raw", ""))
527
+
528
+ 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))
538
+
539
+ raw_fwd = base64.urlsafe_b64encode(fwd.as_bytes()).decode("utf-8")
540
+
541
+ async with httpx.AsyncClient() as client:
542
+ resp = await client.post(
543
+ f"https://gmail.googleapis.com/gmail/v1/users/{self._mailbox}/messages/send",
544
+ headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
545
+ json={"raw": raw_fwd},
546
+ )
547
+ resp.raise_for_status()
548
+ logger.info("Message %s forwarded via Gmail API to %s", message_id, to)
549
+
450
550
  # -----------------------------------------------------------------------
451
551
  # Move / Mark read
452
552
  # -----------------------------------------------------------------------