fix(backend/hitl): preserve user timezone when resuming execution from review

- Add user_timezone to ExecutionContext when resuming after review approval
- Fetch user to get timezone preference, defaulting to UTC if not set
- Make error deduplication more general using contextvars
- Replace global flag with log_once_per_task() helper for task-scoped logging
- Prevents log spam when processing batches (embeddings, etc.)

Addresses CodeRabbit comment about ExecutionContext not being exhaustive.
This commit is contained in:
Zamil Majdy
2026-01-22 19:59:45 -05:00
parent edd4c96aa6
commit 614ed8cf82
2 changed files with 53 additions and 10 deletions

View File

@@ -19,6 +19,8 @@ from backend.data.human_review import (
has_pending_reviews_for_graph_exec,
process_all_reviews_for_execution,
)
from backend.data.model import USER_TIMEZONE_NOT_SET
from backend.data.user import get_user_by_id
from backend.executor.utils import add_graph_execution
from .model import PendingHumanReviewModel, ReviewRequest, ReviewResponse
@@ -238,13 +240,21 @@ async def process_review_action(
first_review = next(iter(updated_reviews.values()))
try:
# Fetch user and settings to build complete execution context
user = await get_user_by_id(user_id)
settings = await get_graph_settings(
user_id=user_id, graph_id=first_review.graph_id
)
# Preserve user's timezone preference when resuming execution
user_timezone = (
user.timezone if user.timezone != USER_TIMEZONE_NOT_SET else "UTC"
)
execution_context = ExecutionContext(
human_in_the_loop_safe_mode=settings.human_in_the_loop_safe_mode,
sensitive_action_safe_mode=settings.sensitive_action_safe_mode,
user_timezone=user_timezone,
)
await add_graph_execution(

View File

@@ -6,6 +6,7 @@ Handles generation and storage of OpenAI embeddings for all content types
"""
import asyncio
import contextvars
import logging
import time
from typing import Any
@@ -21,8 +22,11 @@ from backend.util.json import dumps
logger = logging.getLogger(__name__)
# Track if we've already logged the missing API key error
_missing_api_key_logged = False
# Context variable to track errors logged in the current task/operation
# This prevents spamming the same error multiple times when processing batches
_logged_errors: contextvars.ContextVar[set[str]] = contextvars.ContextVar(
"_logged_errors", default=set()
)
# OpenAI embedding model configuration
EMBEDDING_MODEL = "text-embedding-3-small"
@@ -33,6 +37,38 @@ EMBEDDING_DIM = 1536
EMBEDDING_MAX_TOKENS = 8191
def log_once_per_task(error_key: str, log_fn, message: str, **kwargs) -> bool:
"""
Log an error/warning only once per task/operation to avoid log spam.
Uses contextvars to track what has been logged in the current async context.
Useful when processing batches where the same error might occur for many items.
Args:
error_key: Unique identifier for this error type
log_fn: Logger function to call (e.g., logger.error, logger.warning)
message: Message to log
**kwargs: Additional arguments to pass to log_fn
Returns:
True if the message was logged, False if it was suppressed (already logged)
Example:
log_once_per_task("missing_api_key", logger.error, "API key not set")
"""
logged = _logged_errors.get()
if error_key in logged:
return False
# Log the message with a note that it will only appear once
log_fn(f"{message} (This message will only be shown once per task.)", **kwargs)
# Mark as logged
logged.add(error_key)
_logged_errors.set(logged)
return True
def build_searchable_text(
name: str,
description: str,
@@ -72,17 +108,14 @@ async def generate_embedding(text: str) -> list[float] | None:
Returns None if embedding generation fails.
Fail-fast: no retries to maintain consistency with approval flow.
"""
global _missing_api_key_logged
try:
client = get_openai_client()
if not client:
if not _missing_api_key_logged:
logger.error(
"openai_internal_api_key not set, cannot generate embeddings. "
"This message will only be shown once."
)
_missing_api_key_logged = True
log_once_per_task(
"openai_api_key_missing",
logger.error,
"openai_internal_api_key not set, cannot generate embeddings",
)
return None
# Truncate text to token limit using tiktoken