improvements async

This commit is contained in:
Nicolò Boschi 2025-10-30 19:40:39 +01:00
parent bc7d9fe07f
commit 8c698d6dfb
7 changed files with 2771 additions and 3171 deletions

File diff suppressed because it is too large Load diff

View file

@ -154,7 +154,7 @@ async def answer_question(memory: TemporalSemanticMemory, agent_id: str, questio
try:
client = AsyncOpenAI()
response = await client.beta.chat.completions.parse(
model="gpt-4o-mini",
model="gpt-5",
messages=[
{
"role": "system",
@ -165,8 +165,7 @@ async def answer_question(memory: TemporalSemanticMemory, agent_id: str, questio
"content": f"Context:\n{context}\n\nQuestion: {question}\n\nAnswer:"
}
],
temperature=0,
max_tokens=8000,
response_format=QuestionAnswer
)
answer = response.choices[0].message.parsed

View file

@ -5,8 +5,10 @@ Uses spaCy for entity extraction and implements resolution logic
to disambiguate entities across memory units.
"""
import spacy
import asyncpg
from typing import List, Dict, Optional, Set
from difflib import SequenceMatcher
from datetime import datetime, timezone
# Load spaCy model (singleton)
@ -90,21 +92,22 @@ class EntityResolver:
Resolves entities to canonical IDs with disambiguation.
"""
def __init__(self, db_conn):
def __init__(self, pool: asyncpg.Pool):
"""
Initialize entity resolver.
Args:
db_conn: psycopg2 database connection
pool: asyncpg connection pool
"""
self.conn = db_conn
self.pool = pool
def resolve_entities_batch(
async def resolve_entities_batch(
self,
agent_id: str,
entities_data: List[Dict],
context: str,
unit_event_date,
conn=None,
) -> List[str]:
"""
Resolve multiple entities in batch (MUCH faster than sequential).
@ -117,6 +120,7 @@ class EntityResolver:
entities_data: List of dicts with 'text', 'type', 'nearby_entities'
context: Context where entities appear
unit_event_date: When this unit was created
conn: Optional connection to use (if None, acquires from pool)
Returns:
List of entity IDs in same order as input
@ -124,9 +128,13 @@ class EntityResolver:
if not entities_data:
return []
cursor = self.conn.cursor()
if conn is None:
async with self.pool.acquire() as conn:
return await self._resolve_entities_batch_impl(conn, agent_id, entities_data, context, unit_event_date)
else:
return await self._resolve_entities_batch_impl(conn, agent_id, entities_data, context, unit_event_date)
try:
async def _resolve_entities_batch_impl(self, conn, agent_id: str, entities_data: List[Dict], context: str, unit_event_date) -> List[str]:
import time
start = time.time()
@ -145,22 +153,25 @@ class EntityResolver:
entity_texts = list(set(e[1]['text'] for e in entities_list))
# Query candidates for all texts at once
from psycopg2.extras import execute_values
cursor.execute(
type_candidates = await conn.fetch(
"""
SELECT canonical_name, id, metadata, last_seen, mention_count
FROM entities
WHERE agent_id = %s AND entity_type = %s
WHERE agent_id = $1 AND entity_type = $2
""",
(agent_id, entity_type)
agent_id, entity_type
)
type_candidates = cursor.fetchall()
# Filter candidates in memory (faster than complex SQL for small datasets)
for entity_text in entity_texts:
matching = []
entity_text_lower = entity_text.lower()
for canonical_name, ent_id, metadata, last_seen, mention_count in type_candidates:
for row in type_candidates:
canonical_name = row['canonical_name']
ent_id = row['id']
metadata = row['metadata']
last_seen = row['last_seen']
mention_count = row['mention_count']
canonical_lower = canonical_name.lower()
# Same matching logic as before
if (entity_text_lower == canonical_lower or
@ -227,15 +238,12 @@ class EntityResolver:
# Batch update existing entities
if entities_to_update:
from psycopg2.extras import execute_values
execute_values(
cursor,
await conn.executemany(
"""
UPDATE entities SET
mention_count = mention_count + 1,
last_seen = data.last_seen
FROM (VALUES %s) AS data(id, last_seen)
WHERE entities.id = data.id::uuid
last_seen = $2
WHERE id = $1::uuid
""",
entities_to_update
)
@ -243,18 +251,15 @@ class EntityResolver:
# Batch create new entities
if entities_to_create:
for idx, entity_data in entities_to_create:
entity_id = self._create_entity(
cursor, agent_id, entity_data['text'],
entity_id = await self._create_entity(
conn, agent_id, entity_data['text'],
entity_data['type'], unit_event_date
)
entity_ids[idx] = entity_id
return entity_ids
finally:
cursor.close()
def resolve_entity(
async def resolve_entity(
self,
agent_id: str,
entity_text: str,
@ -277,32 +282,28 @@ class EntityResolver:
Returns:
Entity ID (creates new entity if needed)
"""
cursor = self.conn.cursor()
try:
async with self.pool.acquire() as conn:
# Find candidate entities with same type and similar name
cursor.execute(
candidates = await conn.fetch(
"""
SELECT id, canonical_name, metadata, last_seen
FROM entities
WHERE agent_id = %s
AND entity_type = %s
WHERE agent_id = $1
AND entity_type = $2
AND (
canonical_name ILIKE %s
OR canonical_name ILIKE %s
OR %s ILIKE canonical_name || '%%'
canonical_name ILIKE $3
OR canonical_name ILIKE $4
OR $3 ILIKE canonical_name || '%%'
)
ORDER BY mention_count DESC
""",
(agent_id, entity_type, entity_text, f"%{entity_text}%", entity_text)
agent_id, entity_type, entity_text, f"%{entity_text}%"
)
candidates = cursor.fetchall()
if not candidates:
# New entity - create it
return self._create_entity(
cursor, agent_id, entity_text, entity_type, unit_event_date
return await self._create_entity(
conn, agent_id, entity_text, entity_type, unit_event_date
)
# Score candidates based on:
@ -317,7 +318,11 @@ class EntityResolver:
nearby_entity_set = {e['text'].lower() for e in nearby_entities if e['text'] != entity_text}
for candidate_id, canonical_name, metadata, last_seen in candidates:
for row in candidates:
candidate_id = row['id']
canonical_name = row['canonical_name']
metadata = row['metadata']
last_seen = row['last_seen']
score = 0.0
# 1. Name similarity (0-1)
@ -331,21 +336,21 @@ class EntityResolver:
# 2. Co-occurring entities (0-0.5)
# Get entities that co-occurred with this candidate before
# Use the materialized co-occurrence cache for fast lookup
cursor.execute(
co_entity_rows = await conn.fetch(
"""
SELECT e.canonical_name, ec.cooccurrence_count
FROM entity_cooccurrences ec
JOIN entities e ON (
CASE
WHEN ec.entity_id_1 = %s THEN ec.entity_id_2
WHEN ec.entity_id_2 = %s THEN ec.entity_id_1
WHEN ec.entity_id_1 = $1 THEN ec.entity_id_2
WHEN ec.entity_id_2 = $1 THEN ec.entity_id_1
END = e.id
)
WHERE ec.entity_id_1 = %s OR ec.entity_id_2 = %s
WHERE ec.entity_id_1 = $1 OR ec.entity_id_2 = $1
""",
(candidate_id, candidate_id, candidate_id, candidate_id)
candidate_id
)
co_entities = {row[0].lower() for row in cursor.fetchall()}
co_entities = {r['canonical_name'].lower() for r in co_entity_rows}
# Check overlap with nearby entities
overlap = len(nearby_entity_set & co_entities)
@ -371,28 +376,25 @@ class EntityResolver:
if best_score > threshold:
# Update entity
cursor.execute(
await conn.execute(
"""
UPDATE entities
SET mention_count = mention_count + 1,
last_seen = %s
WHERE id = %s
last_seen = $1
WHERE id = $2
""",
(unit_event_date, best_candidate)
unit_event_date, best_candidate
)
return best_candidate
else:
# Not confident - create new entity
return self._create_entity(
cursor, agent_id, entity_text, entity_type, unit_event_date
return await self._create_entity(
conn, agent_id, entity_text, entity_type, unit_event_date
)
finally:
cursor.close()
def _create_entity(
async def _create_entity(
self,
cursor,
conn,
agent_id: str,
entity_text: str,
entity_type: str,
@ -402,7 +404,7 @@ class EntityResolver:
Create a new entity.
Args:
cursor: Database cursor
conn: Database connection
agent_id: Agent ID
entity_text: Entity text
entity_type: Entity type
@ -411,18 +413,17 @@ class EntityResolver:
Returns:
Entity ID
"""
cursor.execute(
entity_id = await conn.fetchval(
"""
INSERT INTO entities (agent_id, canonical_name, entity_type, first_seen, last_seen, mention_count)
VALUES (%s, %s, %s, %s, %s, 1)
VALUES ($1, $2, $3, $4, $5, 1)
RETURNING id
""",
(agent_id, entity_text, entity_type, event_date, event_date)
agent_id, entity_text, entity_type, event_date, event_date
)
entity_id = cursor.fetchone()[0]
return entity_id
def link_unit_to_entity(self, unit_id: str, entity_id: str):
async def link_unit_to_entity(self, unit_id: str, entity_id: str):
"""
Link a memory unit to an entity.
Also updates co-occurrence cache with other entities in the same unit.
@ -431,45 +432,41 @@ class EntityResolver:
unit_id: Memory unit ID
entity_id: Entity ID
"""
cursor = self.conn.cursor()
try:
async with self.pool.acquire() as conn:
# Insert unit-entity link
cursor.execute(
await conn.execute(
"""
INSERT INTO unit_entities (unit_id, entity_id)
VALUES (%s, %s)
VALUES ($1, $2)
ON CONFLICT DO NOTHING
""",
(unit_id, entity_id)
unit_id, entity_id
)
# Update co-occurrence cache: find other entities in this unit
cursor.execute(
rows = await conn.fetch(
"""
SELECT entity_id
FROM unit_entities
WHERE unit_id = %s AND entity_id != %s
WHERE unit_id = $1 AND entity_id != $2
""",
(unit_id, entity_id)
unit_id, entity_id
)
other_entities = [row[0] for row in cursor.fetchall()]
other_entities = [row['entity_id'] for row in rows]
# Update co-occurrences for each pair
for other_entity_id in other_entities:
self._update_cooccurrence(cursor, entity_id, other_entity_id)
await self._update_cooccurrence(conn, entity_id, other_entity_id)
finally:
cursor.close()
def _update_cooccurrence(self, cursor, entity_id_1: str, entity_id_2: str):
async def _update_cooccurrence(self, conn, entity_id_1: str, entity_id_2: str):
"""
Update the co-occurrence cache for two entities.
Uses CHECK constraint ordering (entity_id_1 < entity_id_2) to avoid duplicates.
Args:
cursor: Database cursor
conn: Database connection
entity_id_1: First entity ID
entity_id_2: Second entity ID
"""
@ -477,19 +474,19 @@ class EntityResolver:
if entity_id_1 > entity_id_2:
entity_id_1, entity_id_2 = entity_id_2, entity_id_1
cursor.execute(
await conn.execute(
"""
INSERT INTO entity_cooccurrences (entity_id_1, entity_id_2, cooccurrence_count, last_cooccurred)
VALUES (%s, %s, 1, NOW())
VALUES ($1, $2, 1, NOW())
ON CONFLICT (entity_id_1, entity_id_2)
DO UPDATE SET
cooccurrence_count = entity_cooccurrences.cooccurrence_count + 1,
last_cooccurred = NOW()
""",
(entity_id_1, entity_id_2)
entity_id_1, entity_id_2
)
def link_units_to_entities_batch(self, unit_entity_pairs: List[tuple[str, str]]):
async def link_units_to_entities_batch(self, unit_entity_pairs: List[tuple[str, str]], conn=None):
"""
Link multiple memory units to entities in batch (MUCH faster than sequential).
@ -497,19 +494,23 @@ class EntityResolver:
Args:
unit_entity_pairs: List of (unit_id, entity_id) tuples
conn: Optional connection to use (if None, acquires from pool)
"""
if not unit_entity_pairs:
return
cursor = self.conn.cursor()
try:
if conn is None:
async with self.pool.acquire() as conn:
return await self._link_units_to_entities_batch_impl(conn, unit_entity_pairs)
else:
return await self._link_units_to_entities_batch_impl(conn, unit_entity_pairs)
async def _link_units_to_entities_batch_impl(self, conn, unit_entity_pairs: List[tuple[str, str]]):
# Batch insert all unit-entity links
from psycopg2.extras import execute_values
execute_values(
cursor,
await conn.executemany(
"""
INSERT INTO unit_entities (unit_id, entity_id)
VALUES %s
VALUES ($1, $2)
ON CONFLICT DO NOTHING
""",
unit_entity_pairs
@ -540,13 +541,11 @@ class EntityResolver:
# Batch update co-occurrences
if cooccurrence_pairs:
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
execute_values(
cursor,
await conn.executemany(
"""
INSERT INTO entity_cooccurrences (entity_id_1, entity_id_2, cooccurrence_count, last_cooccurred)
VALUES %s
VALUES ($1, $2, $3, $4)
ON CONFLICT (entity_id_1, entity_id_2)
DO UPDATE SET
cooccurrence_count = entity_cooccurrences.cooccurrence_count + 1,
@ -555,10 +554,7 @@ class EntityResolver:
[(e1, e2, 1, now) for e1, e2 in cooccurrence_pairs]
)
finally:
cursor.close()
def get_units_by_entity(self, entity_id: str, limit: int = 100) -> List[str]:
async def get_units_by_entity(self, entity_id: str, limit: int = 100) -> List[str]:
"""
Get all units that mention an entity.
@ -569,23 +565,20 @@ class EntityResolver:
Returns:
List of unit IDs
"""
cursor = self.conn.cursor()
try:
cursor.execute(
async with self.pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT unit_id
FROM unit_entities
WHERE entity_id = %s
WHERE entity_id = $1
ORDER BY unit_id
LIMIT %s
LIMIT $2
""",
(entity_id, limit)
entity_id, limit
)
return [row[0] for row in cursor.fetchall()]
finally:
cursor.close()
return [row['unit_id'] for row in rows]
def get_entity_by_text(
async def get_entity_by_text(
self,
agent_id: str,
entity_text: str,
@ -602,33 +595,29 @@ class EntityResolver:
Returns:
Entity ID if found, None otherwise
"""
cursor = self.conn.cursor()
try:
async with self.pool.acquire() as conn:
if entity_type:
cursor.execute(
row = await conn.fetchrow(
"""
SELECT id FROM entities
WHERE agent_id = %s
AND entity_type = %s
AND canonical_name ILIKE %s
WHERE agent_id = $1
AND entity_type = $2
AND canonical_name ILIKE $3
ORDER BY mention_count DESC
LIMIT 1
""",
(agent_id, entity_type, entity_text)
agent_id, entity_type, entity_text
)
else:
cursor.execute(
row = await conn.fetchrow(
"""
SELECT id FROM entities
WHERE agent_id = %s
AND canonical_name ILIKE %s
WHERE agent_id = $1
AND canonical_name ILIKE $2
ORDER BY mention_count DESC
LIMIT 1
""",
(agent_id, entity_text)
agent_id, entity_text
)
row = cursor.fetchone()
return row[0] if row else None
finally:
cursor.close()
return row['id'] if row else None

View file

@ -11,9 +11,7 @@ This implements a sophisticated memory architecture that combines:
import os
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
import psycopg2
from psycopg2.extras import RealDictCursor, execute_values
from pgvector.psycopg2 import register_vector
import asyncpg
from sentence_transformers import SentenceTransformer
from dotenv import load_dotenv
import asyncio
@ -92,7 +90,7 @@ class TemporalSemanticMemory:
"""
load_dotenv()
# Initialize PostgreSQL connection
# Initialize PostgreSQL connection URL
self.db_url = db_url or os.getenv("DATABASE_URL")
if not self.db_url:
raise ValueError(
@ -100,21 +98,40 @@ class TemporalSemanticMemory:
"Set DATABASE_URL environment variable."
)
self.conn = psycopg2.connect(self.db_url)
register_vector(self.conn)
# Connection pool (created lazily on first use)
self._pool = None
self._pool_lock = asyncio.Lock()
# Initialize entity resolver
self.entity_resolver = EntityResolver(self.conn)
# Initialize entity resolver (will be created with pool)
self.entity_resolver = None
# Initialize local embedding model (384 dimensions)
print(f"Loading embedding model: {embedding_model}...")
self.embedding_model = SentenceTransformer(embedding_model)
print(f"✓ Model loaded (embedding dim: {self.embedding_model.get_sentence_embedding_dimension()})")
def __del__(self):
"""Clean up database connection."""
if hasattr(self, 'conn') and self.conn:
self.conn.close()
async def _get_pool(self) -> asyncpg.Pool:
"""Get or create the connection pool (lazy initialization)."""
if self._pool is None:
async with self._pool_lock:
if self._pool is None:
self._pool = await asyncpg.create_pool(
self.db_url,
min_size=2,
max_size=10,
command_timeout=60,
statement_cache_size=0 # Disable prepared statement cache
)
# Initialize entity resolver with pool
if self.entity_resolver is None:
self.entity_resolver = EntityResolver(self._pool)
return self._pool
async def close(self):
"""Close the connection pool."""
if self._pool is not None:
await self._pool.close()
self._pool = None
def _generate_embedding(self, text: str) -> List[float]:
"""
@ -161,9 +178,9 @@ class TemporalSemanticMemory:
except Exception as e:
raise Exception(f"Failed to generate batch embeddings: {str(e)}")
def _find_duplicate_facts_batch(
async def _find_duplicate_facts_batch(
self,
cursor,
conn,
agent_id: str,
texts: List[str],
embeddings: List[List[float]],
@ -178,7 +195,7 @@ class TemporalSemanticMemory:
within the time window. Uses pgvector cosine similarity for efficiency.
Args:
cursor: Database cursor
conn: Database connection
agent_id: Agent identifier
texts: List of fact texts to check
embeddings: Corresponding embeddings
@ -196,20 +213,21 @@ class TemporalSemanticMemory:
for text, embedding in zip(texts, embeddings):
# Query for similar facts within time window
cursor.execute(
# Convert embedding list to string for asyncpg vector type
embedding_str = str(embedding)
result = await conn.fetchrow(
"""
SELECT id, text, 1 - (embedding <=> %s::vector) AS similarity
SELECT id, text, 1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE agent_id = %s
AND event_date BETWEEN %s AND %s
AND 1 - (embedding <=> %s::vector) > %s
WHERE agent_id = $2
AND event_date BETWEEN $3 AND $4
AND 1 - (embedding <=> $1::vector) > $5
ORDER BY similarity DESC
LIMIT 1
""",
(embedding, agent_id, time_lower, time_upper, embedding, similarity_threshold)
embedding_str, agent_id, time_lower, time_upper, similarity_threshold
)
result = cursor.fetchone()
if result:
is_duplicate.append(True)
else:
@ -369,14 +387,16 @@ class TemporalSemanticMemory:
print(f"[2] Generate embeddings (parallel): {len(all_embeddings)} embeddings in {time.time() - step_start:.3f}s")
# Step 3: Process everything in ONE database transaction
cursor = self.conn.cursor()
pool = await self._get_pool()
async with pool.acquire() as conn:
async with conn.transaction():
try:
# Deduplication check for all facts
step_start = time.time()
all_is_duplicate = []
for sentence, embedding, fact_date in zip(all_fact_texts, all_embeddings, all_fact_dates):
dup_flags = self._find_duplicate_facts_batch(
cursor, agent_id, [sentence], [embedding], fact_date
dup_flags = await self._find_duplicate_facts_batch(
conn, agent_id, [sentence], [embedding], fact_date
)
all_is_duplicate.extend(dup_flags)
@ -396,54 +416,50 @@ class TemporalSemanticMemory:
# Batch insert ALL units
step_start = time.time()
from psycopg2.extras import execute_values
unit_data = [
(agent_id, sentence, context, embedding, date, 0) # access_count starts at 0
for sentence, context, embedding, date in zip(
filtered_sentences, filtered_contexts, filtered_embeddings, filtered_dates
)
]
results = execute_values(
cursor,
# Convert embeddings to strings for asyncpg vector type
filtered_embeddings_str = [str(emb) for emb in filtered_embeddings]
results = await conn.fetch(
"""
INSERT INTO memory_units (agent_id, text, context, embedding, event_date, access_count)
VALUES %s
SELECT * FROM unnest($1::text[], $2::text[], $3::text[], $4::vector[], $5::timestamptz[], $6::integer[])
RETURNING id
""",
unit_data,
fetch=True
[agent_id] * len(filtered_sentences),
filtered_sentences,
filtered_contexts,
filtered_embeddings_str,
filtered_dates,
[0] * len(filtered_sentences)
)
created_unit_ids = [str(row[0]) for row in results]
created_unit_ids = [str(row['id']) for row in results]
print(f"[5] Batch insert units: {len(created_unit_ids)} units in {time.time() - step_start:.3f}s")
# Process entities for ALL units
step_start = time.time()
all_entity_links = self._extract_entities_batch_optimized(
cursor, agent_id, created_unit_ids, filtered_sentences, "", filtered_dates
all_entity_links = await self._extract_entities_batch_optimized(
conn, agent_id, created_unit_ids, filtered_sentences, "", filtered_dates
)
print(f"[6] Extract entities (batched): {time.time() - step_start:.3f}s")
# Create temporal links
step_start = time.time()
self._create_temporal_links_batch_per_fact(cursor, agent_id, created_unit_ids)
await self._create_temporal_links_batch_per_fact(conn, agent_id, created_unit_ids)
print(f"[7] Batch create temporal links: {time.time() - step_start:.3f}s")
# Create semantic links
step_start = time.time()
self._create_semantic_links_batch(cursor, agent_id, created_unit_ids, filtered_embeddings)
await self._create_semantic_links_batch(conn, agent_id, created_unit_ids, filtered_embeddings)
print(f"[8] Batch create semantic links: {time.time() - step_start:.3f}s")
# Insert entity links
step_start = time.time()
if all_entity_links:
self._insert_entity_links_batch(cursor, all_entity_links)
await self._insert_entity_links_batch(conn, all_entity_links)
print(f"[9] Batch insert entity links: {time.time() - step_start:.3f}s")
# Commit everything
# Transaction auto-commits on success
commit_start = time.time()
self.conn.commit()
print(f"[10] Commit: {time.time() - commit_start:.3f}s")
# Map created unit IDs back to original content items
@ -467,137 +483,10 @@ class TemporalSemanticMemory:
return result_unit_ids
except Exception as e:
self.conn.rollback()
# Transaction auto-rolls back on exception
import traceback
traceback.print_exc()
raise Exception(f"Failed to store batch memory: {str(e)}")
finally:
cursor.close()
def _create_temporal_links(
self,
cursor,
agent_id: str,
unit_id: str,
event_date: datetime,
time_window_hours: int = 24,
):
"""
Create temporal links to recent memories.
Links this unit to other units that occurred within a time window.
Args:
cursor: Database cursor
agent_id: Agent ID
unit_id: ID of the current unit
event_date: When this event occurred
time_window_hours: Size of the temporal window
"""
try:
# Get recent units within time window
cursor.execute(
"""
SELECT id, event_date
FROM memory_units
WHERE agent_id = %s
AND id != %s
AND event_date >= %s
ORDER BY event_date DESC
LIMIT 10
""",
(agent_id, unit_id, event_date - timedelta(hours=time_window_hours))
)
recent_units = cursor.fetchall()
# Create links to recent units
links = []
for recent_id, recent_event_date in recent_units:
# Calculate temporal proximity weight
time_diff_hours = abs((event_date - recent_event_date).total_seconds() / 3600)
weight = max(0.3, 1.0 - (time_diff_hours / time_window_hours))
links.append((unit_id, recent_id, 'temporal', weight, None))
if links:
execute_values(
cursor,
"""
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
VALUES %s
ON CONFLICT (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid)) DO NOTHING
""",
links
)
except Exception as e:
print(f"ERROR: Failed to create temporal links: {str(e)}")
import traceback
traceback.print_exc()
# Re-raise to trigger rollback at put_async level
raise
def _create_semantic_links(
self,
cursor,
agent_id: str,
unit_id: str,
embedding: List[float],
top_k: int = 5,
threshold: float = 0.7,
):
"""
Create semantic links to similar memories.
Links this unit to other units with similar meaning.
Args:
cursor: Database cursor
agent_id: Agent ID
unit_id: ID of the current unit
embedding: Embedding of the current unit
top_k: Number of similar units to link to
threshold: Minimum similarity threshold
"""
try:
# Find similar units using vector similarity
cursor.execute(
"""
SELECT id, 1 - (embedding <=> %s::vector) AS similarity
FROM memory_units
WHERE agent_id = %s
AND id != %s
AND embedding IS NOT NULL
AND (1 - (embedding <=> %s::vector)) >= %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""",
(embedding, agent_id, unit_id, embedding, threshold, embedding, top_k)
)
similar_units = cursor.fetchall()
# Create links to similar units
links = []
for similar_id, similarity in similar_units:
links.append((unit_id, similar_id, 'semantic', float(similarity), None))
if links:
execute_values(
cursor,
"""
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
VALUES %s
ON CONFLICT (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid)) DO NOTHING
""",
links
)
except Exception as e:
print(f"ERROR: Failed to create semantic links: {str(e)}")
import traceback
traceback.print_exc()
# Re-raise to trigger rollback at put_async level
raise
def search(
self,
@ -653,8 +542,8 @@ class TemporalSemanticMemory:
Returns:
List of memory units with their weights, sorted by relevance
"""
cursor = self.conn.cursor(cursor_factory=RealDictCursor)
pool = await self._get_pool()
async with pool.acquire() as conn:
search_start = time.time()
print(f"\n[SEARCH] Starting search for query: '{query[:50]}...' (thinking_budget={thinking_budget}, top_k={top_k})")
@ -666,21 +555,22 @@ class TemporalSemanticMemory:
# Step 2: Find entry points
step_start = time.time()
cursor.execute(
# Convert embedding to string for asyncpg
query_embedding_str = str(query_embedding)
entry_points = await conn.fetch(
"""
SELECT id, text, context, event_date, access_count, embedding,
1 - (embedding <=> %s::vector) AS similarity
SELECT id, text, context, event_date, access_count,
1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE agent_id = %s
WHERE agent_id = $2
AND embedding IS NOT NULL
AND (1 - (embedding <=> %s::vector)) >= 0.5
ORDER BY embedding <=> %s::vector
AND (1 - (embedding <=> $1::vector)) >= 0.5
ORDER BY embedding <=> $1::vector
LIMIT 3
""",
(query_embedding, agent_id, query_embedding, query_embedding)
query_embedding_str, agent_id
)
entry_points = cursor.fetchall()
print(f" [2] Find entry points: {len(entry_points)} found in {time.time() - step_start:.3f}s")
if not entry_points:
@ -722,27 +612,26 @@ class TemporalSemanticMemory:
# Update access counts for batch
substep_start = time.time()
node_ids = [str(node[0]["id"]) for node in nodes_to_process]
cursor.execute(
"UPDATE memory_units SET access_count = access_count + 1 WHERE id::text = ANY(%s)",
(node_ids,)
await conn.execute(
"UPDATE memory_units SET access_count = access_count + 1 WHERE id::text = ANY($1)",
node_ids
)
update_access_time += time.time() - substep_start
# Query neighbors for ALL nodes in batch at once
substep_start = time.time()
cursor.execute(
all_neighbors = await conn.fetch(
"""
SELECT ml.from_unit_id, ml.to_unit_id, ml.weight,
mu.text, mu.context, mu.event_date, mu.access_count, mu.embedding
mu.text, mu.context, mu.event_date, mu.access_count
FROM memory_links ml
JOIN memory_units mu ON ml.to_unit_id = mu.id
WHERE ml.from_unit_id::text = ANY(%s)
WHERE ml.from_unit_id::text = ANY($1)
AND ml.weight >= 0.1
ORDER BY ml.from_unit_id, ml.weight DESC
""",
(node_ids,)
node_ids
)
all_neighbors = cursor.fetchall()
query_neighbors_time += time.time() - substep_start
# Group neighbors by from_unit_id
@ -841,27 +730,20 @@ class TemporalSemanticMemory:
print(f" [3.3] Query neighbors: {query_neighbors_time:.3f}s ({num_batches} batched queries)")
print(f" [3.4] Process neighbors: {process_neighbors_time:.3f}s")
step_start = time.time()
self.conn.commit()
print(f" [4] Commit: {time.time() - step_start:.3f}s")
# Step 4: Sort by final weight and return top results
step_start = time.time()
results.sort(key=lambda x: x["weight"], reverse=True)
top_results = results[:top_k]
print(f" [5] Sort and return top {top_k}: {time.time() - step_start:.3f}s")
print(f" [4] Sort and return top {top_k}: {time.time() - step_start:.3f}s")
print(f"[SEARCH] Complete: {len(top_results)} results in {time.time() - search_start:.3f}s\n")
return top_results
except Exception as e:
print(f"[SEARCH] ERROR after {time.time() - search_start:.3f}s: {str(e)}")
self.conn.rollback()
raise Exception(f"Failed to search memories: {str(e)}")
finally:
cursor.close()
def delete_agent(self, agent_id: str) -> Dict[str, int]:
async def delete_agent(self, agent_id: str) -> Dict[str, int]:
"""
Delete all data for a specific agent (multi-tenant cleanup).
@ -879,23 +761,19 @@ class TemporalSemanticMemory:
Returns:
Dictionary with counts of deleted items
"""
cursor = self.conn.cursor()
pool = await self._get_pool()
async with pool.acquire() as conn:
async with conn.transaction():
try:
# Count before deletion for reporting
cursor.execute("SELECT COUNT(*) FROM memory_units WHERE agent_id = %s", (agent_id,))
units_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM entities WHERE agent_id = %s", (agent_id,))
entities_count = cursor.fetchone()[0]
units_count = await conn.fetchval("SELECT COUNT(*) FROM memory_units WHERE agent_id = $1", agent_id)
entities_count = await conn.fetchval("SELECT COUNT(*) FROM entities WHERE agent_id = $1", agent_id)
# Delete memory units (cascades to unit_entities, memory_links)
cursor.execute("DELETE FROM memory_units WHERE agent_id = %s", (agent_id,))
await conn.execute("DELETE FROM memory_units WHERE agent_id = $1", agent_id)
# Delete entities (cascades to unit_entities, entity_cooccurrences, memory_links with entity_id)
cursor.execute("DELETE FROM entities WHERE agent_id = %s", (agent_id,))
self.conn.commit()
await conn.execute("DELETE FROM entities WHERE agent_id = $1", agent_id)
return {
"memory_units_deleted": units_count,
@ -903,64 +781,11 @@ class TemporalSemanticMemory:
}
except Exception as e:
self.conn.rollback()
raise Exception(f"Failed to delete agent data: {str(e)}")
finally:
cursor.close()
def get_memory_graph_data(self, agent_id: str = None) -> Tuple[List[Dict], List[Dict]]:
"""
Get memory graph data for visualization.
Args:
agent_id: Optional agent ID (if None, returns all data)
Returns:
Tuple of (units, links) for visualization
"""
cursor = self.conn.cursor(cursor_factory=RealDictCursor)
try:
# Get all units (optionally filtered by agent)
if agent_id:
cursor.execute(
"SELECT id, text, context, event_date, access_count FROM memory_units WHERE agent_id = %s",
(agent_id,)
)
else:
cursor.execute(
"SELECT id, text, context, event_date, access_count FROM memory_units"
)
units = [dict(row) for row in cursor.fetchall()]
# Get all links (optionally filtered by agent)
if agent_id:
cursor.execute(
"""
SELECT ml.from_unit_id, ml.to_unit_id, ml.link_type, ml.weight
FROM memory_links ml
JOIN memory_units mu1 ON ml.from_unit_id = mu1.id
JOIN memory_units mu2 ON ml.to_unit_id = mu2.id
WHERE mu1.agent_id = %s
""",
(agent_id,)
)
else:
cursor.execute(
"SELECT from_unit_id, to_unit_id, link_type, weight FROM memory_links"
)
links = [dict(row) for row in cursor.fetchall()]
return units, links
except Exception as e:
raise Exception(f"Failed to get memory graph data: {str(e)}")
finally:
cursor.close()
def _extract_entities_batch_optimized(
async def _extract_entities_batch_optimized(
self,
cursor,
conn,
agent_id: str,
unit_ids: List[str],
sentences: List[str],
@ -1024,11 +849,12 @@ class TemporalSemanticMemory:
indices = [idx for idx, _ in entities_group]
entities_data = [entity_data for _, entity_data in entities_group]
batch_resolved = self.entity_resolver.resolve_entities_batch(
batch_resolved = await self.entity_resolver.resolve_entities_batch(
agent_id=agent_id,
entities_data=entities_data,
context=context,
unit_event_date=fact_date
unit_event_date=fact_date,
conn=conn
)
for idx, entity_id in zip(indices, batch_resolved):
@ -1049,7 +875,7 @@ class TemporalSemanticMemory:
unit_entity_pairs.append((unit_id, entity_id))
# Batch insert all unit-entity links (MUCH faster!)
self.entity_resolver.link_units_to_entities_batch(unit_entity_pairs)
await self.entity_resolver.link_units_to_entities_batch(unit_entity_pairs, conn=conn)
print(f" [6.2.3] Create unit-entity links (batched): {len(unit_entity_pairs)} links in {time.time() - substep_6_2_3_start:.3f}s")
print(f" [6.2] Entity resolution (batched): {len(all_entities_flat)} entities resolved in {time.time() - step_6_2_start:.3f}s")
@ -1067,15 +893,15 @@ class TemporalSemanticMemory:
# For each entity, find all units that reference it (one query per entity)
entity_to_units = {}
for entity_id in all_entity_ids:
cursor.execute(
rows = await conn.fetch(
"""
SELECT unit_id
FROM unit_entities
WHERE entity_id = %s
WHERE entity_id = $1
""",
(entity_id,)
entity_id
)
entity_to_units[entity_id] = [row[0] for row in cursor.fetchall()]
entity_to_units[entity_id] = [row['unit_id'] for row in rows]
# Create bidirectional links between units that share entities
links = []
@ -1098,71 +924,9 @@ class TemporalSemanticMemory:
# Re-raise to trigger rollback at put_async level
raise
def _create_temporal_links_batch(
async def _create_temporal_links_batch_per_fact(
self,
cursor,
agent_id: str,
unit_ids: List[str],
event_date: datetime,
time_window_hours: int = 24,
):
"""
Create temporal links for multiple units in one batch query.
Uses a single query to find all relevant temporal connections.
"""
if not unit_ids:
return
try:
from psycopg2.extras import execute_values
# Get ALL recent units within time window (single query)
# Cast string IDs to UUIDs for comparison
cursor.execute(
"""
SELECT id, event_date
FROM memory_units
WHERE agent_id = %s
AND id::text != ALL(%s)
AND event_date >= %s
ORDER BY event_date DESC
""",
(agent_id, unit_ids, event_date - timedelta(hours=time_window_hours))
)
recent_units = cursor.fetchall()
# Create links from each new unit to all recent units
links = []
for unit_id in unit_ids:
for recent_id, recent_event_date in recent_units:
# Calculate temporal proximity weight
time_diff_hours = abs((event_date - recent_event_date).total_seconds() / 3600)
weight = max(0.3, 1.0 - (time_diff_hours / time_window_hours))
links.append((unit_id, recent_id, 'temporal', weight, None))
if links:
execute_values(
cursor,
"""
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
VALUES %s
ON CONFLICT (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid)) DO NOTHING
""",
links
)
except Exception as e:
print(f"ERROR: Failed to create temporal links: {str(e)}")
import traceback
traceback.print_exc()
# Re-raise to trigger rollback at put_async level
raise
def _create_temporal_links_batch_per_fact(
self,
cursor,
conn,
agent_id: str,
unit_ids: List[str],
time_window_hours: int = 24,
@ -1177,55 +941,50 @@ class TemporalSemanticMemory:
return
try:
from psycopg2.extras import execute_values
# Get the event_date for each new unit
cursor.execute(
rows = await conn.fetch(
"""
SELECT id, event_date
FROM memory_units
WHERE id::text = ANY(%s)
WHERE id::text = ANY($1)
""",
(unit_ids,)
unit_ids
)
new_units = {str(row[0]): row[1] for row in cursor.fetchall()}
new_units = {str(row['id']): row['event_date'] for row in rows}
# Create links based on each unit's individual event_date
links = []
for unit_id, unit_event_date in new_units.items():
# Find units within the time window of THIS specific unit
cursor.execute(
recent_units = await conn.fetch(
"""
SELECT id, event_date
FROM memory_units
WHERE agent_id = %s
AND id != %s
AND event_date BETWEEN %s AND %s
WHERE agent_id = $1
AND id != $2
AND event_date BETWEEN $3 AND $4
ORDER BY event_date DESC
LIMIT 10
""",
(
agent_id,
unit_id,
unit_event_date - timedelta(hours=time_window_hours),
unit_event_date + timedelta(hours=time_window_hours)
)
)
recent_units = cursor.fetchall()
for recent_id, recent_event_date in recent_units:
for recent_row in recent_units:
recent_id = recent_row['id']
recent_event_date = recent_row['event_date']
# Calculate temporal proximity weight
time_diff_hours = abs((unit_event_date - recent_event_date).total_seconds() / 3600)
weight = max(0.3, 1.0 - (time_diff_hours / time_window_hours))
links.append((unit_id, recent_id, 'temporal', weight, None))
links.append((unit_id, str(recent_id), 'temporal', weight, None))
if links:
execute_values(
cursor,
await conn.executemany(
"""
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
VALUES %s
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid)) DO NOTHING
""",
links
@ -1238,9 +997,9 @@ class TemporalSemanticMemory:
# Re-raise to trigger rollback at put_async level
raise
def _create_semantic_links_batch(
async def _create_semantic_links_batch(
self,
cursor,
conn,
agent_id: str,
unit_ids: List[str],
embeddings: List[List[float]],
@ -1256,37 +1015,36 @@ class TemporalSemanticMemory:
return
try:
from psycopg2.extras import execute_values
all_links = []
for unit_id, embedding in zip(unit_ids, embeddings):
# Find similar units using vector similarity
cursor.execute(
# Convert embedding to string for asyncpg
embedding_str = str(embedding)
similar_units = await conn.fetch(
"""
SELECT id, 1 - (embedding <=> %s::vector) AS similarity
SELECT id, 1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE agent_id = %s
AND id != %s
WHERE agent_id = $2
AND id != $3
AND embedding IS NOT NULL
AND (1 - (embedding <=> %s::vector)) >= %s
ORDER BY embedding <=> %s::vector
LIMIT %s
AND (1 - (embedding <=> $1::vector)) >= $4
ORDER BY embedding <=> $1::vector
LIMIT $5
""",
(embedding, agent_id, unit_id, embedding, threshold, embedding, top_k)
embedding_str, agent_id, unit_id, threshold, top_k
)
similar_units = cursor.fetchall()
for similar_id, similarity in similar_units:
all_links.append((unit_id, similar_id, 'semantic', float(similarity), None))
for row in similar_units:
similar_id = row['id']
similarity = row['similarity']
all_links.append((unit_id, str(similar_id), 'semantic', float(similarity), None))
if all_links:
execute_values(
cursor,
await conn.executemany(
"""
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
VALUES %s
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid)) DO NOTHING
""",
all_links
@ -1299,18 +1057,16 @@ class TemporalSemanticMemory:
# Re-raise to trigger rollback at put_async level
raise
def _insert_entity_links_batch(self, cursor, links: List[tuple]):
async def _insert_entity_links_batch(self, conn, links: List[tuple]):
"""Insert all entity links in a single batch."""
if not links:
return
try:
from psycopg2.extras import execute_values
execute_values(
cursor,
await conn.executemany(
"""
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
VALUES %s
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid)) DO NOTHING
""",
links

View file

@ -5,8 +5,7 @@ description = "Temporal + Semantic + Entity Memory System for AI agents using Po
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"psycopg2-binary>=2.9.0",
"pgvector>=0.2.0",
"asyncpg>=0.29.0",
"python-dotenv>=1.0.0",
"openai>=1.0.0",
"pydantic>=2.0.0",

View file

@ -3,9 +3,10 @@ Pytest configuration and shared fixtures.
"""
import pytest
import os
import asyncio
from dotenv import load_dotenv
from memory import TemporalSemanticMemory
import psycopg2
import asyncpg
load_dotenv()
@ -29,19 +30,19 @@ def clean_agent(memory):
agent_id = "test"
# Clean up before test
memory.delete_agent(agent_id)
asyncio.run(memory.delete_agent(agent_id))
yield agent_id
# Clean up after test
memory.delete_agent(agent_id)
asyncio.run(memory.delete_agent(agent_id))
@pytest.fixture
def db_connection():
async def db_connection():
"""
Provide a database connection for direct DB queries in tests.
"""
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
conn = await asyncpg.connect(os.getenv('DATABASE_URL'))
yield conn
conn.close()
await conn.close()

102
uv.lock
View file

@ -29,6 +29,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097 },
]
[[package]]
name = "asyncpg"
version = "0.30.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 },
{ url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 },
{ url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 },
{ url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 },
{ url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 },
{ url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 },
{ url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 },
{ url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 },
{ url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 },
{ url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 },
{ url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 },
{ url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 },
{ url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 },
{ url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 },
{ url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 },
{ url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 },
{ url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 },
{ url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 },
{ url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 },
{ url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 },
{ url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 },
{ url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 },
{ url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 },
{ url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 },
]
[[package]]
name = "blis"
version = "1.3.0"
@ -1009,13 +1041,12 @@ name = "memory-poc"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "asyncpg" },
{ name = "langchain-text-splitters" },
{ name = "matplotlib" },
{ name = "networkx" },
{ name = "nltk" },
{ name = "openai" },
{ name = "pgvector" },
{ name = "psycopg2-binary" },
{ name = "pydantic" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
@ -1028,13 +1059,12 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "asyncpg", specifier = ">=0.29.0" },
{ name = "langchain-text-splitters", specifier = ">=0.3.0" },
{ name = "matplotlib", specifier = ">=3.7.0" },
{ name = "networkx", specifier = ">=3.0" },
{ name = "nltk", specifier = ">=3.8.0" },
{ name = "openai", specifier = ">=1.0.0" },
{ name = "pgvector", specifier = ">=0.2.0" },
{ name = "psycopg2-binary", specifier = ">=2.9.0" },
{ name = "pydantic", specifier = ">=2.0.0" },
{ name = "pytest", specifier = ">=7.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.21.0" },
@ -1418,18 +1448,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
]
[[package]]
name = "pgvector"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/43/9a0fb552ab4fd980680c2037962e331820f67585df740bedc4a2b50faf20/pgvector-0.4.1.tar.gz", hash = "sha256:83d3a1c044ff0c2f1e95d13dfb625beb0b65506cfec0941bfe81fd0ad44f4003", size = 30646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/21/b5735d5982892c878ff3d01bb06e018c43fc204428361ee9fc25a1b2125c/pgvector-0.4.1-py3-none-any.whl", hash = "sha256:34bb4e99e1b13d08a2fe82dda9f860f15ddcd0166fbb25bffe15821cbfeb7362", size = 27086 },
]
[[package]]
name = "pillow"
version = "12.0.0"
@ -1559,58 +1577,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/8c/d3e30f80b2ef21f267f09f0b7d18995adccc928ede5b73ea3fe54e1303f4/preshed-3.0.10-cp313-cp313-win_amd64.whl", hash = "sha256:97e0e2edfd25a7dfba799b49b3c5cc248ad0318a76edd9d5fd2c82aa3d5c64ed", size = 115769 },
]
[[package]]
name = "psycopg2-binary"
version = "2.9.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452 },
{ url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957 },
{ url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955 },
{ url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007 },
{ url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012 },
{ url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881 },
{ url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985 },
{ url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039 },
{ url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477 },
{ url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842 },
{ url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894 },
{ url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603 },
{ url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509 },
{ url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159 },
{ url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234 },
{ url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236 },
{ url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083 },
{ url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281 },
{ url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010 },
{ url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641 },
{ url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940 },
{ url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147 },
{ url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572 },
{ url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529 },
{ url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242 },
{ url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258 },
{ url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295 },
{ url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133 },
{ url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383 },
{ url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168 },
{ url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712 },
{ url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549 },
{ url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215 },
{ url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567 },
{ url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755 },
{ url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646 },
{ url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701 },
{ url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293 },
{ url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184 },
{ url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650 },
{ url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663 },
{ url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737 },
{ url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643 },
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913 },
]
[[package]]
name = "pydantic"
version = "2.12.3"