fix(memory): replace deprecated datetime.utcnow() across memory subsystem#5839
fix(memory): replace deprecated datetime.utcnow() across memory subsystem#5839Ghraven wants to merge 1 commit into
Conversation
…stem datetime.utcnow() was deprecated in Python 3.12 and is scheduled for removal in Python 3.15. It returns a naive UTC datetime, which also causes TypeError when compared against timezone-aware datetimes. Replace all occurrences in the memory subsystem (4 files, 9 sites) with datetime.now(timezone.utc): - memory/types.py: MemoryRecord.created_at/last_accessed default_factory and age_seconds calculation - memory/encoding_flow.py: now = datetime.utcnow() in encoding helper - memory/unified_memory.py: now = datetime.utcnow() in cleanup method - memory/storage/lancedb_storage.py: created_at, last_accessed, and two isoformat() timestamp writes All four files compile cleanly. No behaviour change for new records; existing stored naive timestamps remain readable since we only changed how new timestamps are produced.
📝 WalkthroughWalkthroughThe PR standardizes datetime handling across the memory module by replacing all naive UTC timestamps ( ChangesMemory module timezone standardization
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/crewai/src/crewai/memory/storage/lancedb_storage.py (1)
268-274:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNormalize parsed datetimes to UTC-aware in
_parse_dt.Lines 272 and 274 can return naive datetimes. Line 272 directly returns datetime objects that may lack timezone info, and line 274 can return naive datetimes when input strings lack timezone information (i.e., strings not ending with "Z" or timezone suffix). This causes a
TypeErrorduring datetime comparisons and sorting operations (e.g.,records.sort(key=lambda r: r.created_at)or comparisons likedt < oldest), especially when mixing legacy naive values with new UTC-aware datetimes written by this PR.Proposed fix
def _parse_dt(val: Any) -> datetime: if val is None: return datetime.now(timezone.utc) if isinstance(val, datetime): - return val + return ( + val + if val.tzinfo is not None + else val.replace(tzinfo=timezone.utc) + ) s = str(val) - return datetime.fromisoformat(s.replace("Z", "+00:00")) + dt = datetime.fromisoformat(s.replace("Z", "+00:00")) + return dt if dt.tzinfo is not None else dt.replace(tzinfo=timezone.utc)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/memory/storage/lancedb_storage.py` around lines 268 - 274, The _parse_dt helper may return naive datetimes causing TypeError on comparisons; ensure every branch returns a UTC-aware datetime by normalizing values to timezone.utc: when val is a datetime, if dt.tzinfo is None set tzinfo=timezone.utc (or convert via astimezone(timezone.utc)); when parsing strings (after the "Z" -> "+00:00" replacement and datetime.fromisoformat), check the parsed object's tzinfo and set it to timezone.utc if missing; keep the None case returning datetime.now(timezone.utc). Update _parse_dt, and reference datetime.fromisoformat, _parse_dt, and timezone.utc in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/crewai/src/crewai/memory/types.py`:
- Around line 371-372: The subtraction fails for naive datetimes in the age
calculation; before computing age_seconds and age_days, normalize
record.created_at to UTC if it's naive by checking record.created_at.tzinfo is
None and, in that case, set record.created_at =
record.created_at.replace(tzinfo=timezone.utc) (leave it unchanged if already
aware), then compute age_seconds = (datetime.now(timezone.utc) -
record.created_at).total_seconds() and age_days = max(age_seconds / 86400.0,
0.0).
---
Outside diff comments:
In `@lib/crewai/src/crewai/memory/storage/lancedb_storage.py`:
- Around line 268-274: The _parse_dt helper may return naive datetimes causing
TypeError on comparisons; ensure every branch returns a UTC-aware datetime by
normalizing values to timezone.utc: when val is a datetime, if dt.tzinfo is None
set tzinfo=timezone.utc (or convert via astimezone(timezone.utc)); when parsing
strings (after the "Z" -> "+00:00" replacement and datetime.fromisoformat),
check the parsed object's tzinfo and set it to timezone.utc if missing; keep the
None case returning datetime.now(timezone.utc). Update _parse_dt, and reference
datetime.fromisoformat, _parse_dt, and timezone.utc in your changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1ab9a2d0-9c6e-402a-b157-f46c58f4164c
📒 Files selected for processing (4)
lib/crewai/src/crewai/memory/encoding_flow.pylib/crewai/src/crewai/memory/storage/lancedb_storage.pylib/crewai/src/crewai/memory/types.pylib/crewai/src/crewai/memory/unified_memory.py
| age_seconds = (datetime.now(timezone.utc) - record.created_at).total_seconds() | ||
| age_days = max(age_seconds / 86400.0, 0.0) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and examine the target file
fd -t f "types.py" lib/crewai/src/crewai/memory/ && cat -n lib/crewai/src/crewai/memory/types.py | sed -n '365,380p'Repository: crewAIInc/crewAI
Length of output: 808
🏁 Script executed:
# Check if there are other references to created_at in the memory module
rg "created_at" lib/crewai/src/crewai/memory/ -nRepository: crewAIInc/crewAI
Length of output: 2720
🏁 Script executed:
# Look for tzinfo checks or timezone imports in the types.py file
rg "tzinfo|timezone" lib/crewai/src/crewai/memory/types.py -nRepository: crewAIInc/crewAI
Length of output: 315
🏁 Script executed:
# Find _parse_dt function definition
rg "_parse_dt" lib/crewai/src/crewai/memory/ -A 10 -B 2Repository: crewAIInc/crewAI
Length of output: 5433
🏁 Script executed:
# Verify that datetime operations with mixed aware/naive raise TypeError
python3 << 'EOF'
from datetime import datetime, timezone
# Test: aware - naive = TypeError
aware = datetime.now(timezone.utc)
naive = datetime.now()
try:
result = aware - naive
print(f"No error (unexpected): {result}")
except TypeError as e:
print(f"TypeError raised (expected): {e}")
EOFRepository: crewAIInc/crewAI
Length of output: 144
🏁 Script executed:
# Check if there's any existing timezone handling in compute_composite_score
rg "compute_composite_score" lib/crewai/src/crewai/memory/types.py -B 5 -A 20Repository: crewAIInc/crewAI
Length of output: 1975
Handle naive created_at before age math.
Line 371 raises TypeError: can't subtract offset-naive and offset-aware datetimes when record.created_at is timezone-naive, which occurs with records from qdrant storage or legacy data. This breaks recall scoring when processing mixed datasets.
Proposed fix
def compute_composite_score(
@@
- age_seconds = (datetime.now(timezone.utc) - record.created_at).total_seconds()
+ created_at = record.created_at
+ if created_at.tzinfo is None:
+ created_at = created_at.replace(tzinfo=timezone.utc)
+ age_seconds = (datetime.now(timezone.utc) - created_at).total_seconds()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/src/crewai/memory/types.py` around lines 371 - 372, The
subtraction fails for naive datetimes in the age calculation; before computing
age_seconds and age_days, normalize record.created_at to UTC if it's naive by
checking record.created_at.tzinfo is None and, in that case, set
record.created_at = record.created_at.replace(tzinfo=timezone.utc) (leave it
unchanged if already aware), then compute age_seconds =
(datetime.now(timezone.utc) - record.created_at).total_seconds() and age_days =
max(age_seconds / 86400.0, 0.0).
Summary
datetime.utcnow()was deprecated in Python 3.12 (PEP 2038) and is scheduled for removal in Python 3.15. It also returns a naive UTC datetime, which is a common source ofTypeError: can't compare offset-naive and offset-aware datetimesonce other parts of the codebase start using timezone-aware datetimes.This PR replaces all occurrences across the memory subsystem (4 files, 9 sites) with
datetime.now(timezone.utc).Files changed
memory/types.pyMemoryRecord.created_at/last_accesseddefault_factory, plusage_secondscalculationmemory/encoding_flow.pynow = datetime.utcnow()in encoding helpermemory/unified_memory.pynow = datetime.utcnow()in cleanup methodmemory/storage/lancedb_storage.pycreated_at,last_accessed, and two.isoformat()timestamp writesVerification
All four files compile cleanly:
No remaining
utcnowin the memory package:Happy to extend this to other parts of the codebase (
streaming.py,tools/structured_tool.py, etc.) as a follow-up if you'd prefer to keep each PR focused.Summary by CodeRabbit