improvements async
This commit is contained in:
parent
bc7d9fe07f
commit
8c698d6dfb
7 changed files with 2771 additions and 3171 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,137 +128,138 @@ 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:
|
||||
import time
|
||||
start = time.time()
|
||||
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()
|
||||
|
||||
# Group entities by type for efficient querying
|
||||
entities_by_type = {}
|
||||
for idx, entity_data in enumerate(entities_data):
|
||||
entity_type = entity_data['type']
|
||||
if entity_type not in entities_by_type:
|
||||
entities_by_type[entity_type] = []
|
||||
entities_by_type[entity_type].append((idx, entity_data))
|
||||
# Group entities by type for efficient querying
|
||||
entities_by_type = {}
|
||||
for idx, entity_data in enumerate(entities_data):
|
||||
entity_type = entity_data['type']
|
||||
if entity_type not in entities_by_type:
|
||||
entities_by_type[entity_type] = []
|
||||
entities_by_type[entity_type].append((idx, entity_data))
|
||||
|
||||
# Query ALL candidates for each type in batch
|
||||
all_candidates = {} # Maps (entity_type, entity_text) -> list of candidates
|
||||
for entity_type, entities_list in entities_by_type.items():
|
||||
# Extract unique entity texts for this type
|
||||
entity_texts = list(set(e[1]['text'] for e in entities_list))
|
||||
# Query ALL candidates for each type in batch
|
||||
all_candidates = {} # Maps (entity_type, entity_text) -> list of candidates
|
||||
for entity_type, entities_list in entities_by_type.items():
|
||||
# Extract unique entity texts for this type
|
||||
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(
|
||||
"""
|
||||
SELECT canonical_name, id, metadata, last_seen, mention_count
|
||||
FROM entities
|
||||
WHERE agent_id = %s AND entity_type = %s
|
||||
""",
|
||||
(agent_id, entity_type)
|
||||
# Query candidates for all texts at once
|
||||
type_candidates = await conn.fetch(
|
||||
"""
|
||||
SELECT canonical_name, id, metadata, last_seen, mention_count
|
||||
FROM entities
|
||||
WHERE agent_id = $1 AND entity_type = $2
|
||||
""",
|
||||
agent_id, entity_type
|
||||
)
|
||||
|
||||
# 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 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
|
||||
entity_text_lower in canonical_lower or
|
||||
canonical_lower in entity_text_lower):
|
||||
matching.append((ent_id, canonical_name, metadata, last_seen, mention_count))
|
||||
all_candidates[(entity_type, entity_text)] = matching
|
||||
|
||||
# Resolve each entity using pre-fetched candidates
|
||||
entity_ids = [None] * len(entities_data)
|
||||
entities_to_update = [] # (entity_id, unit_event_date)
|
||||
entities_to_create = [] # (idx, entity_data)
|
||||
|
||||
for idx, entity_data in enumerate(entities_data):
|
||||
entity_text = entity_data['text']
|
||||
entity_type = entity_data['type']
|
||||
nearby_entities = entity_data.get('nearby_entities', [])
|
||||
|
||||
candidates = all_candidates.get((entity_type, entity_text), [])
|
||||
|
||||
if not candidates:
|
||||
# Will create new entity
|
||||
entities_to_create.append((idx, entity_data))
|
||||
continue
|
||||
|
||||
# Score candidates (same logic as before but with pre-fetched data)
|
||||
best_candidate = None
|
||||
best_score = 0.0
|
||||
best_name_similarity = 0.0
|
||||
|
||||
nearby_entity_set = {e['text'].lower() for e in nearby_entities if e['text'] != entity_text}
|
||||
|
||||
for candidate_id, canonical_name, metadata, last_seen, mention_count in candidates:
|
||||
score = 0.0
|
||||
|
||||
# Name similarity
|
||||
name_similarity = SequenceMatcher(
|
||||
None,
|
||||
entity_text.lower(),
|
||||
canonical_name.lower()
|
||||
).ratio()
|
||||
score += name_similarity * 0.5
|
||||
|
||||
# Temporal proximity
|
||||
if last_seen:
|
||||
days_diff = abs((unit_event_date - last_seen).total_seconds() / 86400)
|
||||
if days_diff < 7:
|
||||
temporal_score = max(0, 1.0 - (days_diff / 7))
|
||||
score += temporal_score * 0.2
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_candidate = candidate_id
|
||||
best_name_similarity = name_similarity
|
||||
|
||||
# Apply threshold
|
||||
threshold = 0.4 if entity_type == 'PERSON' and best_name_similarity >= 0.95 else 0.6
|
||||
|
||||
if best_score > threshold:
|
||||
entity_ids[idx] = best_candidate
|
||||
entities_to_update.append((best_candidate, unit_event_date))
|
||||
else:
|
||||
entities_to_create.append((idx, entity_data))
|
||||
|
||||
# Batch update existing entities
|
||||
if entities_to_update:
|
||||
await conn.executemany(
|
||||
"""
|
||||
UPDATE entities SET
|
||||
mention_count = mention_count + 1,
|
||||
last_seen = $2
|
||||
WHERE id = $1::uuid
|
||||
""",
|
||||
entities_to_update
|
||||
)
|
||||
|
||||
# Batch create new entities
|
||||
if entities_to_create:
|
||||
for idx, entity_data in entities_to_create:
|
||||
entity_id = await self._create_entity(
|
||||
conn, agent_id, entity_data['text'],
|
||||
entity_data['type'], unit_event_date
|
||||
)
|
||||
type_candidates = cursor.fetchall()
|
||||
entity_ids[idx] = entity_id
|
||||
|
||||
# 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:
|
||||
canonical_lower = canonical_name.lower()
|
||||
# Same matching logic as before
|
||||
if (entity_text_lower == canonical_lower or
|
||||
entity_text_lower in canonical_lower or
|
||||
canonical_lower in entity_text_lower):
|
||||
matching.append((ent_id, canonical_name, metadata, last_seen, mention_count))
|
||||
all_candidates[(entity_type, entity_text)] = matching
|
||||
return entity_ids
|
||||
|
||||
# Resolve each entity using pre-fetched candidates
|
||||
entity_ids = [None] * len(entities_data)
|
||||
entities_to_update = [] # (entity_id, unit_event_date)
|
||||
entities_to_create = [] # (idx, entity_data)
|
||||
|
||||
for idx, entity_data in enumerate(entities_data):
|
||||
entity_text = entity_data['text']
|
||||
entity_type = entity_data['type']
|
||||
nearby_entities = entity_data.get('nearby_entities', [])
|
||||
|
||||
candidates = all_candidates.get((entity_type, entity_text), [])
|
||||
|
||||
if not candidates:
|
||||
# Will create new entity
|
||||
entities_to_create.append((idx, entity_data))
|
||||
continue
|
||||
|
||||
# Score candidates (same logic as before but with pre-fetched data)
|
||||
best_candidate = None
|
||||
best_score = 0.0
|
||||
best_name_similarity = 0.0
|
||||
|
||||
nearby_entity_set = {e['text'].lower() for e in nearby_entities if e['text'] != entity_text}
|
||||
|
||||
for candidate_id, canonical_name, metadata, last_seen, mention_count in candidates:
|
||||
score = 0.0
|
||||
|
||||
# Name similarity
|
||||
name_similarity = SequenceMatcher(
|
||||
None,
|
||||
entity_text.lower(),
|
||||
canonical_name.lower()
|
||||
).ratio()
|
||||
score += name_similarity * 0.5
|
||||
|
||||
# Temporal proximity
|
||||
if last_seen:
|
||||
days_diff = abs((unit_event_date - last_seen).total_seconds() / 86400)
|
||||
if days_diff < 7:
|
||||
temporal_score = max(0, 1.0 - (days_diff / 7))
|
||||
score += temporal_score * 0.2
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_candidate = candidate_id
|
||||
best_name_similarity = name_similarity
|
||||
|
||||
# Apply threshold
|
||||
threshold = 0.4 if entity_type == 'PERSON' and best_name_similarity >= 0.95 else 0.6
|
||||
|
||||
if best_score > threshold:
|
||||
entity_ids[idx] = best_candidate
|
||||
entities_to_update.append((best_candidate, unit_event_date))
|
||||
else:
|
||||
entities_to_create.append((idx, entity_data))
|
||||
|
||||
# Batch update existing entities
|
||||
if entities_to_update:
|
||||
from psycopg2.extras import execute_values
|
||||
execute_values(
|
||||
cursor,
|
||||
"""
|
||||
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
|
||||
""",
|
||||
entities_to_update
|
||||
)
|
||||
|
||||
# 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_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,68 +494,67 @@ 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:
|
||||
# Batch insert all unit-entity links
|
||||
from psycopg2.extras import execute_values
|
||||
execute_values(
|
||||
cursor,
|
||||
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
|
||||
await conn.executemany(
|
||||
"""
|
||||
INSERT INTO unit_entities (unit_id, entity_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
""",
|
||||
unit_entity_pairs
|
||||
)
|
||||
|
||||
# Build map of unit -> entities for co-occurrence calculation
|
||||
# Use sets to avoid duplicate entities in the same unit
|
||||
unit_to_entities = {}
|
||||
for unit_id, entity_id in unit_entity_pairs:
|
||||
if unit_id not in unit_to_entities:
|
||||
unit_to_entities[unit_id] = set()
|
||||
unit_to_entities[unit_id].add(entity_id)
|
||||
|
||||
# Update co-occurrences for all pairs in each unit
|
||||
cooccurrence_pairs = set() # Use set to avoid duplicates
|
||||
for unit_id, entity_ids in unit_to_entities.items():
|
||||
entity_list = list(entity_ids) # Convert set to list for iteration
|
||||
# For each pair of entities in this unit, create co-occurrence
|
||||
for i, entity_id_1 in enumerate(entity_list):
|
||||
for entity_id_2 in entity_list[i+1:]:
|
||||
# Skip if same entity (shouldn't happen with set, but be safe)
|
||||
if entity_id_1 == entity_id_2:
|
||||
continue
|
||||
# Ensure consistent ordering (entity_id_1 < entity_id_2)
|
||||
if entity_id_1 > entity_id_2:
|
||||
entity_id_1, entity_id_2 = entity_id_2, entity_id_1
|
||||
cooccurrence_pairs.add((entity_id_1, entity_id_2))
|
||||
|
||||
# Batch update co-occurrences
|
||||
if cooccurrence_pairs:
|
||||
now = datetime.now(timezone.utc)
|
||||
await conn.executemany(
|
||||
"""
|
||||
INSERT INTO unit_entities (unit_id, entity_id)
|
||||
VALUES %s
|
||||
ON CONFLICT DO NOTHING
|
||||
INSERT INTO entity_cooccurrences (entity_id_1, entity_id_2, cooccurrence_count, last_cooccurred)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (entity_id_1, entity_id_2)
|
||||
DO UPDATE SET
|
||||
cooccurrence_count = entity_cooccurrences.cooccurrence_count + 1,
|
||||
last_cooccurred = EXCLUDED.last_cooccurred
|
||||
""",
|
||||
unit_entity_pairs
|
||||
[(e1, e2, 1, now) for e1, e2 in cooccurrence_pairs]
|
||||
)
|
||||
|
||||
# Build map of unit -> entities for co-occurrence calculation
|
||||
# Use sets to avoid duplicate entities in the same unit
|
||||
unit_to_entities = {}
|
||||
for unit_id, entity_id in unit_entity_pairs:
|
||||
if unit_id not in unit_to_entities:
|
||||
unit_to_entities[unit_id] = set()
|
||||
unit_to_entities[unit_id].add(entity_id)
|
||||
|
||||
# Update co-occurrences for all pairs in each unit
|
||||
cooccurrence_pairs = set() # Use set to avoid duplicates
|
||||
for unit_id, entity_ids in unit_to_entities.items():
|
||||
entity_list = list(entity_ids) # Convert set to list for iteration
|
||||
# For each pair of entities in this unit, create co-occurrence
|
||||
for i, entity_id_1 in enumerate(entity_list):
|
||||
for entity_id_2 in entity_list[i+1:]:
|
||||
# Skip if same entity (shouldn't happen with set, but be safe)
|
||||
if entity_id_1 == entity_id_2:
|
||||
continue
|
||||
# Ensure consistent ordering (entity_id_1 < entity_id_2)
|
||||
if entity_id_1 > entity_id_2:
|
||||
entity_id_1, entity_id_2 = entity_id_2, entity_id_1
|
||||
cooccurrence_pairs.add((entity_id_1, entity_id_2))
|
||||
|
||||
# Batch update co-occurrences
|
||||
if cooccurrence_pairs:
|
||||
from datetime import datetime, timezone
|
||||
now = datetime.now(timezone.utc)
|
||||
execute_values(
|
||||
cursor,
|
||||
"""
|
||||
INSERT INTO entity_cooccurrences (entity_id_1, entity_id_2, cooccurrence_count, last_cooccurred)
|
||||
VALUES %s
|
||||
ON CONFLICT (entity_id_1, entity_id_2)
|
||||
DO UPDATE SET
|
||||
cooccurrence_count = entity_cooccurrences.cooccurrence_count + 1,
|
||||
last_cooccurred = EXCLUDED.last_cooccurred
|
||||
""",
|
||||
[(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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
102
uv.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue