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:
|
try:
|
||||||
client = AsyncOpenAI()
|
client = AsyncOpenAI()
|
||||||
response = await client.beta.chat.completions.parse(
|
response = await client.beta.chat.completions.parse(
|
||||||
model="gpt-4o-mini",
|
model="gpt-5",
|
||||||
messages=[
|
messages=[
|
||||||
{
|
{
|
||||||
"role": "system",
|
"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:"
|
"content": f"Context:\n{context}\n\nQuestion: {question}\n\nAnswer:"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
temperature=0,
|
|
||||||
max_tokens=8000,
|
|
||||||
response_format=QuestionAnswer
|
response_format=QuestionAnswer
|
||||||
)
|
)
|
||||||
answer = response.choices[0].message.parsed
|
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.
|
to disambiguate entities across memory units.
|
||||||
"""
|
"""
|
||||||
import spacy
|
import spacy
|
||||||
|
import asyncpg
|
||||||
from typing import List, Dict, Optional, Set
|
from typing import List, Dict, Optional, Set
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
# Load spaCy model (singleton)
|
# Load spaCy model (singleton)
|
||||||
|
|
@ -90,21 +92,22 @@ class EntityResolver:
|
||||||
Resolves entities to canonical IDs with disambiguation.
|
Resolves entities to canonical IDs with disambiguation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, db_conn):
|
def __init__(self, pool: asyncpg.Pool):
|
||||||
"""
|
"""
|
||||||
Initialize entity resolver.
|
Initialize entity resolver.
|
||||||
|
|
||||||
Args:
|
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,
|
self,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
entities_data: List[Dict],
|
entities_data: List[Dict],
|
||||||
context: str,
|
context: str,
|
||||||
unit_event_date,
|
unit_event_date,
|
||||||
|
conn=None,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Resolve multiple entities in batch (MUCH faster than sequential).
|
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'
|
entities_data: List of dicts with 'text', 'type', 'nearby_entities'
|
||||||
context: Context where entities appear
|
context: Context where entities appear
|
||||||
unit_event_date: When this unit was created
|
unit_event_date: When this unit was created
|
||||||
|
conn: Optional connection to use (if None, acquires from pool)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of entity IDs in same order as input
|
List of entity IDs in same order as input
|
||||||
|
|
@ -124,9 +128,13 @@ class EntityResolver:
|
||||||
if not entities_data:
|
if not entities_data:
|
||||||
return []
|
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
|
import time
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
|
|
@ -145,22 +153,25 @@ class EntityResolver:
|
||||||
entity_texts = list(set(e[1]['text'] for e in entities_list))
|
entity_texts = list(set(e[1]['text'] for e in entities_list))
|
||||||
|
|
||||||
# Query candidates for all texts at once
|
# Query candidates for all texts at once
|
||||||
from psycopg2.extras import execute_values
|
type_candidates = await conn.fetch(
|
||||||
cursor.execute(
|
|
||||||
"""
|
"""
|
||||||
SELECT canonical_name, id, metadata, last_seen, mention_count
|
SELECT canonical_name, id, metadata, last_seen, mention_count
|
||||||
FROM entities
|
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)
|
# Filter candidates in memory (faster than complex SQL for small datasets)
|
||||||
for entity_text in entity_texts:
|
for entity_text in entity_texts:
|
||||||
matching = []
|
matching = []
|
||||||
entity_text_lower = entity_text.lower()
|
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()
|
canonical_lower = canonical_name.lower()
|
||||||
# Same matching logic as before
|
# Same matching logic as before
|
||||||
if (entity_text_lower == canonical_lower or
|
if (entity_text_lower == canonical_lower or
|
||||||
|
|
@ -227,15 +238,12 @@ class EntityResolver:
|
||||||
|
|
||||||
# Batch update existing entities
|
# Batch update existing entities
|
||||||
if entities_to_update:
|
if entities_to_update:
|
||||||
from psycopg2.extras import execute_values
|
await conn.executemany(
|
||||||
execute_values(
|
|
||||||
cursor,
|
|
||||||
"""
|
"""
|
||||||
UPDATE entities SET
|
UPDATE entities SET
|
||||||
mention_count = mention_count + 1,
|
mention_count = mention_count + 1,
|
||||||
last_seen = data.last_seen
|
last_seen = $2
|
||||||
FROM (VALUES %s) AS data(id, last_seen)
|
WHERE id = $1::uuid
|
||||||
WHERE entities.id = data.id::uuid
|
|
||||||
""",
|
""",
|
||||||
entities_to_update
|
entities_to_update
|
||||||
)
|
)
|
||||||
|
|
@ -243,18 +251,15 @@ class EntityResolver:
|
||||||
# Batch create new entities
|
# Batch create new entities
|
||||||
if entities_to_create:
|
if entities_to_create:
|
||||||
for idx, entity_data in entities_to_create:
|
for idx, entity_data in entities_to_create:
|
||||||
entity_id = self._create_entity(
|
entity_id = await self._create_entity(
|
||||||
cursor, agent_id, entity_data['text'],
|
conn, agent_id, entity_data['text'],
|
||||||
entity_data['type'], unit_event_date
|
entity_data['type'], unit_event_date
|
||||||
)
|
)
|
||||||
entity_ids[idx] = entity_id
|
entity_ids[idx] = entity_id
|
||||||
|
|
||||||
return entity_ids
|
return entity_ids
|
||||||
|
|
||||||
finally:
|
async def resolve_entity(
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
def resolve_entity(
|
|
||||||
self,
|
self,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
entity_text: str,
|
entity_text: str,
|
||||||
|
|
@ -277,32 +282,28 @@ class EntityResolver:
|
||||||
Returns:
|
Returns:
|
||||||
Entity ID (creates new entity if needed)
|
Entity ID (creates new entity if needed)
|
||||||
"""
|
"""
|
||||||
cursor = self.conn.cursor()
|
async with self.pool.acquire() as conn:
|
||||||
|
|
||||||
try:
|
|
||||||
# Find candidate entities with same type and similar name
|
# Find candidate entities with same type and similar name
|
||||||
cursor.execute(
|
candidates = await conn.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT id, canonical_name, metadata, last_seen
|
SELECT id, canonical_name, metadata, last_seen
|
||||||
FROM entities
|
FROM entities
|
||||||
WHERE agent_id = %s
|
WHERE agent_id = $1
|
||||||
AND entity_type = %s
|
AND entity_type = $2
|
||||||
AND (
|
AND (
|
||||||
canonical_name ILIKE %s
|
canonical_name ILIKE $3
|
||||||
OR canonical_name ILIKE %s
|
OR canonical_name ILIKE $4
|
||||||
OR %s ILIKE canonical_name || '%%'
|
OR $3 ILIKE canonical_name || '%%'
|
||||||
)
|
)
|
||||||
ORDER BY mention_count DESC
|
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:
|
if not candidates:
|
||||||
# New entity - create it
|
# New entity - create it
|
||||||
return self._create_entity(
|
return await self._create_entity(
|
||||||
cursor, agent_id, entity_text, entity_type, unit_event_date
|
conn, agent_id, entity_text, entity_type, unit_event_date
|
||||||
)
|
)
|
||||||
|
|
||||||
# Score candidates based on:
|
# 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}
|
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
|
score = 0.0
|
||||||
|
|
||||||
# 1. Name similarity (0-1)
|
# 1. Name similarity (0-1)
|
||||||
|
|
@ -331,21 +336,21 @@ class EntityResolver:
|
||||||
# 2. Co-occurring entities (0-0.5)
|
# 2. Co-occurring entities (0-0.5)
|
||||||
# Get entities that co-occurred with this candidate before
|
# Get entities that co-occurred with this candidate before
|
||||||
# Use the materialized co-occurrence cache for fast lookup
|
# Use the materialized co-occurrence cache for fast lookup
|
||||||
cursor.execute(
|
co_entity_rows = await conn.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT e.canonical_name, ec.cooccurrence_count
|
SELECT e.canonical_name, ec.cooccurrence_count
|
||||||
FROM entity_cooccurrences ec
|
FROM entity_cooccurrences ec
|
||||||
JOIN entities e ON (
|
JOIN entities e ON (
|
||||||
CASE
|
CASE
|
||||||
WHEN ec.entity_id_1 = %s THEN ec.entity_id_2
|
WHEN ec.entity_id_1 = $1 THEN ec.entity_id_2
|
||||||
WHEN ec.entity_id_2 = %s THEN ec.entity_id_1
|
WHEN ec.entity_id_2 = $1 THEN ec.entity_id_1
|
||||||
END = e.id
|
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
|
# Check overlap with nearby entities
|
||||||
overlap = len(nearby_entity_set & co_entities)
|
overlap = len(nearby_entity_set & co_entities)
|
||||||
|
|
@ -371,28 +376,25 @@ class EntityResolver:
|
||||||
|
|
||||||
if best_score > threshold:
|
if best_score > threshold:
|
||||||
# Update entity
|
# Update entity
|
||||||
cursor.execute(
|
await conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE entities
|
UPDATE entities
|
||||||
SET mention_count = mention_count + 1,
|
SET mention_count = mention_count + 1,
|
||||||
last_seen = %s
|
last_seen = $1
|
||||||
WHERE id = %s
|
WHERE id = $2
|
||||||
""",
|
""",
|
||||||
(unit_event_date, best_candidate)
|
unit_event_date, best_candidate
|
||||||
)
|
)
|
||||||
return best_candidate
|
return best_candidate
|
||||||
else:
|
else:
|
||||||
# Not confident - create new entity
|
# Not confident - create new entity
|
||||||
return self._create_entity(
|
return await self._create_entity(
|
||||||
cursor, agent_id, entity_text, entity_type, unit_event_date
|
conn, agent_id, entity_text, entity_type, unit_event_date
|
||||||
)
|
)
|
||||||
|
|
||||||
finally:
|
async def _create_entity(
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
def _create_entity(
|
|
||||||
self,
|
self,
|
||||||
cursor,
|
conn,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
entity_text: str,
|
entity_text: str,
|
||||||
entity_type: str,
|
entity_type: str,
|
||||||
|
|
@ -402,7 +404,7 @@ class EntityResolver:
|
||||||
Create a new entity.
|
Create a new entity.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cursor: Database cursor
|
conn: Database connection
|
||||||
agent_id: Agent ID
|
agent_id: Agent ID
|
||||||
entity_text: Entity text
|
entity_text: Entity text
|
||||||
entity_type: Entity type
|
entity_type: Entity type
|
||||||
|
|
@ -411,18 +413,17 @@ class EntityResolver:
|
||||||
Returns:
|
Returns:
|
||||||
Entity ID
|
Entity ID
|
||||||
"""
|
"""
|
||||||
cursor.execute(
|
entity_id = await conn.fetchval(
|
||||||
"""
|
"""
|
||||||
INSERT INTO entities (agent_id, canonical_name, entity_type, first_seen, last_seen, mention_count)
|
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
|
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
|
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.
|
Link a memory unit to an entity.
|
||||||
Also updates co-occurrence cache with other entities in the same unit.
|
Also updates co-occurrence cache with other entities in the same unit.
|
||||||
|
|
@ -431,45 +432,41 @@ class EntityResolver:
|
||||||
unit_id: Memory unit ID
|
unit_id: Memory unit ID
|
||||||
entity_id: Entity ID
|
entity_id: Entity ID
|
||||||
"""
|
"""
|
||||||
cursor = self.conn.cursor()
|
async with self.pool.acquire() as conn:
|
||||||
try:
|
|
||||||
# Insert unit-entity link
|
# Insert unit-entity link
|
||||||
cursor.execute(
|
await conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO unit_entities (unit_id, entity_id)
|
INSERT INTO unit_entities (unit_id, entity_id)
|
||||||
VALUES (%s, %s)
|
VALUES ($1, $2)
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
""",
|
""",
|
||||||
(unit_id, entity_id)
|
unit_id, entity_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update co-occurrence cache: find other entities in this unit
|
# Update co-occurrence cache: find other entities in this unit
|
||||||
cursor.execute(
|
rows = await conn.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT entity_id
|
SELECT entity_id
|
||||||
FROM unit_entities
|
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
|
# Update co-occurrences for each pair
|
||||||
for other_entity_id in other_entities:
|
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:
|
async def _update_cooccurrence(self, conn, entity_id_1: str, entity_id_2: str):
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
def _update_cooccurrence(self, cursor, entity_id_1: str, entity_id_2: str):
|
|
||||||
"""
|
"""
|
||||||
Update the co-occurrence cache for two entities.
|
Update the co-occurrence cache for two entities.
|
||||||
|
|
||||||
Uses CHECK constraint ordering (entity_id_1 < entity_id_2) to avoid duplicates.
|
Uses CHECK constraint ordering (entity_id_1 < entity_id_2) to avoid duplicates.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cursor: Database cursor
|
conn: Database connection
|
||||||
entity_id_1: First entity ID
|
entity_id_1: First entity ID
|
||||||
entity_id_2: Second entity ID
|
entity_id_2: Second entity ID
|
||||||
"""
|
"""
|
||||||
|
|
@ -477,19 +474,19 @@ class EntityResolver:
|
||||||
if 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
|
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)
|
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)
|
ON CONFLICT (entity_id_1, entity_id_2)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
cooccurrence_count = entity_cooccurrences.cooccurrence_count + 1,
|
cooccurrence_count = entity_cooccurrences.cooccurrence_count + 1,
|
||||||
last_cooccurred = NOW()
|
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).
|
Link multiple memory units to entities in batch (MUCH faster than sequential).
|
||||||
|
|
||||||
|
|
@ -497,19 +494,23 @@ class EntityResolver:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
unit_entity_pairs: List of (unit_id, entity_id) tuples
|
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:
|
if not unit_entity_pairs:
|
||||||
return
|
return
|
||||||
|
|
||||||
cursor = self.conn.cursor()
|
if conn is None:
|
||||||
try:
|
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
|
# Batch insert all unit-entity links
|
||||||
from psycopg2.extras import execute_values
|
await conn.executemany(
|
||||||
execute_values(
|
|
||||||
cursor,
|
|
||||||
"""
|
"""
|
||||||
INSERT INTO unit_entities (unit_id, entity_id)
|
INSERT INTO unit_entities (unit_id, entity_id)
|
||||||
VALUES %s
|
VALUES ($1, $2)
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
""",
|
""",
|
||||||
unit_entity_pairs
|
unit_entity_pairs
|
||||||
|
|
@ -540,13 +541,11 @@ class EntityResolver:
|
||||||
|
|
||||||
# Batch update co-occurrences
|
# Batch update co-occurrences
|
||||||
if cooccurrence_pairs:
|
if cooccurrence_pairs:
|
||||||
from datetime import datetime, timezone
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
execute_values(
|
await conn.executemany(
|
||||||
cursor,
|
|
||||||
"""
|
"""
|
||||||
INSERT INTO entity_cooccurrences (entity_id_1, entity_id_2, cooccurrence_count, last_cooccurred)
|
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)
|
ON CONFLICT (entity_id_1, entity_id_2)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
cooccurrence_count = entity_cooccurrences.cooccurrence_count + 1,
|
cooccurrence_count = entity_cooccurrences.cooccurrence_count + 1,
|
||||||
|
|
@ -555,10 +554,7 @@ class EntityResolver:
|
||||||
[(e1, e2, 1, now) for e1, e2 in cooccurrence_pairs]
|
[(e1, e2, 1, now) for e1, e2 in cooccurrence_pairs]
|
||||||
)
|
)
|
||||||
|
|
||||||
finally:
|
async def get_units_by_entity(self, entity_id: str, limit: int = 100) -> List[str]:
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
def get_units_by_entity(self, entity_id: str, limit: int = 100) -> List[str]:
|
|
||||||
"""
|
"""
|
||||||
Get all units that mention an entity.
|
Get all units that mention an entity.
|
||||||
|
|
||||||
|
|
@ -569,23 +565,20 @@ class EntityResolver:
|
||||||
Returns:
|
Returns:
|
||||||
List of unit IDs
|
List of unit IDs
|
||||||
"""
|
"""
|
||||||
cursor = self.conn.cursor()
|
async with self.pool.acquire() as conn:
|
||||||
try:
|
rows = await conn.fetch(
|
||||||
cursor.execute(
|
|
||||||
"""
|
"""
|
||||||
SELECT unit_id
|
SELECT unit_id
|
||||||
FROM unit_entities
|
FROM unit_entities
|
||||||
WHERE entity_id = %s
|
WHERE entity_id = $1
|
||||||
ORDER BY unit_id
|
ORDER BY unit_id
|
||||||
LIMIT %s
|
LIMIT $2
|
||||||
""",
|
""",
|
||||||
(entity_id, limit)
|
entity_id, limit
|
||||||
)
|
)
|
||||||
return [row[0] for row in cursor.fetchall()]
|
return [row['unit_id'] for row in rows]
|
||||||
finally:
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
def get_entity_by_text(
|
async def get_entity_by_text(
|
||||||
self,
|
self,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
entity_text: str,
|
entity_text: str,
|
||||||
|
|
@ -602,33 +595,29 @@ class EntityResolver:
|
||||||
Returns:
|
Returns:
|
||||||
Entity ID if found, None otherwise
|
Entity ID if found, None otherwise
|
||||||
"""
|
"""
|
||||||
cursor = self.conn.cursor()
|
async with self.pool.acquire() as conn:
|
||||||
try:
|
|
||||||
if entity_type:
|
if entity_type:
|
||||||
cursor.execute(
|
row = await conn.fetchrow(
|
||||||
"""
|
"""
|
||||||
SELECT id FROM entities
|
SELECT id FROM entities
|
||||||
WHERE agent_id = %s
|
WHERE agent_id = $1
|
||||||
AND entity_type = %s
|
AND entity_type = $2
|
||||||
AND canonical_name ILIKE %s
|
AND canonical_name ILIKE $3
|
||||||
ORDER BY mention_count DESC
|
ORDER BY mention_count DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""",
|
""",
|
||||||
(agent_id, entity_type, entity_text)
|
agent_id, entity_type, entity_text
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
cursor.execute(
|
row = await conn.fetchrow(
|
||||||
"""
|
"""
|
||||||
SELECT id FROM entities
|
SELECT id FROM entities
|
||||||
WHERE agent_id = %s
|
WHERE agent_id = $1
|
||||||
AND canonical_name ILIKE %s
|
AND canonical_name ILIKE $2
|
||||||
ORDER BY mention_count DESC
|
ORDER BY mention_count DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""",
|
""",
|
||||||
(agent_id, entity_text)
|
agent_id, entity_text
|
||||||
)
|
)
|
||||||
|
|
||||||
row = cursor.fetchone()
|
return row['id'] if row else None
|
||||||
return row[0] if row else None
|
|
||||||
finally:
|
|
||||||
cursor.close()
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,7 @@ This implements a sophisticated memory architecture that combines:
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
import psycopg2
|
import asyncpg
|
||||||
from psycopg2.extras import RealDictCursor, execute_values
|
|
||||||
from pgvector.psycopg2 import register_vector
|
|
||||||
from sentence_transformers import SentenceTransformer
|
from sentence_transformers import SentenceTransformer
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
@ -92,7 +90,7 @@ class TemporalSemanticMemory:
|
||||||
"""
|
"""
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Initialize PostgreSQL connection
|
# Initialize PostgreSQL connection URL
|
||||||
self.db_url = db_url or os.getenv("DATABASE_URL")
|
self.db_url = db_url or os.getenv("DATABASE_URL")
|
||||||
if not self.db_url:
|
if not self.db_url:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|
@ -100,21 +98,40 @@ class TemporalSemanticMemory:
|
||||||
"Set DATABASE_URL environment variable."
|
"Set DATABASE_URL environment variable."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.conn = psycopg2.connect(self.db_url)
|
# Connection pool (created lazily on first use)
|
||||||
register_vector(self.conn)
|
self._pool = None
|
||||||
|
self._pool_lock = asyncio.Lock()
|
||||||
|
|
||||||
# Initialize entity resolver
|
# Initialize entity resolver (will be created with pool)
|
||||||
self.entity_resolver = EntityResolver(self.conn)
|
self.entity_resolver = None
|
||||||
|
|
||||||
# Initialize local embedding model (384 dimensions)
|
# Initialize local embedding model (384 dimensions)
|
||||||
print(f"Loading embedding model: {embedding_model}...")
|
print(f"Loading embedding model: {embedding_model}...")
|
||||||
self.embedding_model = SentenceTransformer(embedding_model)
|
self.embedding_model = SentenceTransformer(embedding_model)
|
||||||
print(f"✓ Model loaded (embedding dim: {self.embedding_model.get_sentence_embedding_dimension()})")
|
print(f"✓ Model loaded (embedding dim: {self.embedding_model.get_sentence_embedding_dimension()})")
|
||||||
|
|
||||||
def __del__(self):
|
async def _get_pool(self) -> asyncpg.Pool:
|
||||||
"""Clean up database connection."""
|
"""Get or create the connection pool (lazy initialization)."""
|
||||||
if hasattr(self, 'conn') and self.conn:
|
if self._pool is None:
|
||||||
self.conn.close()
|
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]:
|
def _generate_embedding(self, text: str) -> List[float]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -161,9 +178,9 @@ class TemporalSemanticMemory:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to generate batch embeddings: {str(e)}")
|
raise Exception(f"Failed to generate batch embeddings: {str(e)}")
|
||||||
|
|
||||||
def _find_duplicate_facts_batch(
|
async def _find_duplicate_facts_batch(
|
||||||
self,
|
self,
|
||||||
cursor,
|
conn,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
texts: List[str],
|
texts: List[str],
|
||||||
embeddings: List[List[float]],
|
embeddings: List[List[float]],
|
||||||
|
|
@ -178,7 +195,7 @@ class TemporalSemanticMemory:
|
||||||
within the time window. Uses pgvector cosine similarity for efficiency.
|
within the time window. Uses pgvector cosine similarity for efficiency.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cursor: Database cursor
|
conn: Database connection
|
||||||
agent_id: Agent identifier
|
agent_id: Agent identifier
|
||||||
texts: List of fact texts to check
|
texts: List of fact texts to check
|
||||||
embeddings: Corresponding embeddings
|
embeddings: Corresponding embeddings
|
||||||
|
|
@ -196,20 +213,21 @@ class TemporalSemanticMemory:
|
||||||
|
|
||||||
for text, embedding in zip(texts, embeddings):
|
for text, embedding in zip(texts, embeddings):
|
||||||
# Query for similar facts within time window
|
# 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
|
FROM memory_units
|
||||||
WHERE agent_id = %s
|
WHERE agent_id = $2
|
||||||
AND event_date BETWEEN %s AND %s
|
AND event_date BETWEEN $3 AND $4
|
||||||
AND 1 - (embedding <=> %s::vector) > %s
|
AND 1 - (embedding <=> $1::vector) > $5
|
||||||
ORDER BY similarity DESC
|
ORDER BY similarity DESC
|
||||||
LIMIT 1
|
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:
|
if result:
|
||||||
is_duplicate.append(True)
|
is_duplicate.append(True)
|
||||||
else:
|
else:
|
||||||
|
|
@ -369,14 +387,16 @@ class TemporalSemanticMemory:
|
||||||
print(f"[2] Generate embeddings (parallel): {len(all_embeddings)} embeddings in {time.time() - step_start:.3f}s")
|
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
|
# 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:
|
try:
|
||||||
# Deduplication check for all facts
|
# Deduplication check for all facts
|
||||||
step_start = time.time()
|
step_start = time.time()
|
||||||
all_is_duplicate = []
|
all_is_duplicate = []
|
||||||
for sentence, embedding, fact_date in zip(all_fact_texts, all_embeddings, all_fact_dates):
|
for sentence, embedding, fact_date in zip(all_fact_texts, all_embeddings, all_fact_dates):
|
||||||
dup_flags = self._find_duplicate_facts_batch(
|
dup_flags = await self._find_duplicate_facts_batch(
|
||||||
cursor, agent_id, [sentence], [embedding], fact_date
|
conn, agent_id, [sentence], [embedding], fact_date
|
||||||
)
|
)
|
||||||
all_is_duplicate.extend(dup_flags)
|
all_is_duplicate.extend(dup_flags)
|
||||||
|
|
||||||
|
|
@ -396,54 +416,50 @@ class TemporalSemanticMemory:
|
||||||
|
|
||||||
# Batch insert ALL units
|
# Batch insert ALL units
|
||||||
step_start = time.time()
|
step_start = time.time()
|
||||||
from psycopg2.extras import execute_values
|
# Convert embeddings to strings for asyncpg vector type
|
||||||
unit_data = [
|
filtered_embeddings_str = [str(emb) for emb in filtered_embeddings]
|
||||||
(agent_id, sentence, context, embedding, date, 0) # access_count starts at 0
|
results = await conn.fetch(
|
||||||
for sentence, context, embedding, date in zip(
|
|
||||||
filtered_sentences, filtered_contexts, filtered_embeddings, filtered_dates
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
results = execute_values(
|
|
||||||
cursor,
|
|
||||||
"""
|
"""
|
||||||
INSERT INTO memory_units (agent_id, text, context, embedding, event_date, access_count)
|
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
|
RETURNING id
|
||||||
""",
|
""",
|
||||||
unit_data,
|
[agent_id] * len(filtered_sentences),
|
||||||
fetch=True
|
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")
|
print(f"[5] Batch insert units: {len(created_unit_ids)} units in {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
# Process entities for ALL units
|
# Process entities for ALL units
|
||||||
step_start = time.time()
|
step_start = time.time()
|
||||||
all_entity_links = self._extract_entities_batch_optimized(
|
all_entity_links = await self._extract_entities_batch_optimized(
|
||||||
cursor, agent_id, created_unit_ids, filtered_sentences, "", filtered_dates
|
conn, agent_id, created_unit_ids, filtered_sentences, "", filtered_dates
|
||||||
)
|
)
|
||||||
print(f"[6] Extract entities (batched): {time.time() - step_start:.3f}s")
|
print(f"[6] Extract entities (batched): {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
# Create temporal links
|
# Create temporal links
|
||||||
step_start = time.time()
|
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")
|
print(f"[7] Batch create temporal links: {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
# Create semantic links
|
# Create semantic links
|
||||||
step_start = time.time()
|
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")
|
print(f"[8] Batch create semantic links: {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
# Insert entity links
|
# Insert entity links
|
||||||
step_start = time.time()
|
step_start = time.time()
|
||||||
if all_entity_links:
|
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")
|
print(f"[9] Batch insert entity links: {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
# Commit everything
|
# Transaction auto-commits on success
|
||||||
commit_start = time.time()
|
commit_start = time.time()
|
||||||
self.conn.commit()
|
|
||||||
print(f"[10] Commit: {time.time() - commit_start:.3f}s")
|
print(f"[10] Commit: {time.time() - commit_start:.3f}s")
|
||||||
|
|
||||||
# Map created unit IDs back to original content items
|
# Map created unit IDs back to original content items
|
||||||
|
|
@ -467,137 +483,10 @@ class TemporalSemanticMemory:
|
||||||
return result_unit_ids
|
return result_unit_ids
|
||||||
|
|
||||||
except Exception as e:
|
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)}")
|
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(
|
def search(
|
||||||
self,
|
self,
|
||||||
|
|
@ -653,8 +542,8 @@ class TemporalSemanticMemory:
|
||||||
Returns:
|
Returns:
|
||||||
List of memory units with their weights, sorted by relevance
|
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()
|
search_start = time.time()
|
||||||
print(f"\n[SEARCH] Starting search for query: '{query[:50]}...' (thinking_budget={thinking_budget}, top_k={top_k})")
|
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 2: Find entry points
|
||||||
step_start = time.time()
|
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,
|
SELECT id, text, context, event_date, access_count,
|
||||||
1 - (embedding <=> %s::vector) AS similarity
|
1 - (embedding <=> $1::vector) AS similarity
|
||||||
FROM memory_units
|
FROM memory_units
|
||||||
WHERE agent_id = %s
|
WHERE agent_id = $2
|
||||||
AND embedding IS NOT NULL
|
AND embedding IS NOT NULL
|
||||||
AND (1 - (embedding <=> %s::vector)) >= 0.5
|
AND (1 - (embedding <=> $1::vector)) >= 0.5
|
||||||
ORDER BY embedding <=> %s::vector
|
ORDER BY embedding <=> $1::vector
|
||||||
LIMIT 3
|
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")
|
print(f" [2] Find entry points: {len(entry_points)} found in {time.time() - step_start:.3f}s")
|
||||||
|
|
||||||
if not entry_points:
|
if not entry_points:
|
||||||
|
|
@ -722,27 +612,26 @@ class TemporalSemanticMemory:
|
||||||
# Update access counts for batch
|
# Update access counts for batch
|
||||||
substep_start = time.time()
|
substep_start = time.time()
|
||||||
node_ids = [str(node[0]["id"]) for node in nodes_to_process]
|
node_ids = [str(node[0]["id"]) for node in nodes_to_process]
|
||||||
cursor.execute(
|
await conn.execute(
|
||||||
"UPDATE memory_units SET access_count = access_count + 1 WHERE id::text = ANY(%s)",
|
"UPDATE memory_units SET access_count = access_count + 1 WHERE id::text = ANY($1)",
|
||||||
(node_ids,)
|
node_ids
|
||||||
)
|
)
|
||||||
update_access_time += time.time() - substep_start
|
update_access_time += time.time() - substep_start
|
||||||
|
|
||||||
# Query neighbors for ALL nodes in batch at once
|
# Query neighbors for ALL nodes in batch at once
|
||||||
substep_start = time.time()
|
substep_start = time.time()
|
||||||
cursor.execute(
|
all_neighbors = await conn.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT ml.from_unit_id, ml.to_unit_id, ml.weight,
|
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
|
FROM memory_links ml
|
||||||
JOIN memory_units mu ON ml.to_unit_id = mu.id
|
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
|
AND ml.weight >= 0.1
|
||||||
ORDER BY ml.from_unit_id, ml.weight DESC
|
ORDER BY ml.from_unit_id, ml.weight DESC
|
||||||
""",
|
""",
|
||||||
(node_ids,)
|
node_ids
|
||||||
)
|
)
|
||||||
all_neighbors = cursor.fetchall()
|
|
||||||
query_neighbors_time += time.time() - substep_start
|
query_neighbors_time += time.time() - substep_start
|
||||||
|
|
||||||
# Group neighbors by from_unit_id
|
# 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.3] Query neighbors: {query_neighbors_time:.3f}s ({num_batches} batched queries)")
|
||||||
print(f" [3.4] Process neighbors: {process_neighbors_time:.3f}s")
|
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 4: Sort by final weight and return top results
|
||||||
step_start = time.time()
|
step_start = time.time()
|
||||||
results.sort(key=lambda x: x["weight"], reverse=True)
|
results.sort(key=lambda x: x["weight"], reverse=True)
|
||||||
top_results = results[:top_k]
|
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")
|
print(f"[SEARCH] Complete: {len(top_results)} results in {time.time() - search_start:.3f}s\n")
|
||||||
return top_results
|
return top_results
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[SEARCH] ERROR after {time.time() - search_start:.3f}s: {str(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)}")
|
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).
|
Delete all data for a specific agent (multi-tenant cleanup).
|
||||||
|
|
||||||
|
|
@ -879,23 +761,19 @@ class TemporalSemanticMemory:
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with counts of deleted items
|
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:
|
try:
|
||||||
# Count before deletion for reporting
|
# Count before deletion for reporting
|
||||||
cursor.execute("SELECT COUNT(*) FROM memory_units WHERE agent_id = %s", (agent_id,))
|
units_count = await conn.fetchval("SELECT COUNT(*) FROM memory_units WHERE agent_id = $1", agent_id)
|
||||||
units_count = cursor.fetchone()[0]
|
entities_count = await conn.fetchval("SELECT COUNT(*) FROM entities WHERE agent_id = $1", agent_id)
|
||||||
|
|
||||||
cursor.execute("SELECT COUNT(*) FROM entities WHERE agent_id = %s", (agent_id,))
|
|
||||||
entities_count = cursor.fetchone()[0]
|
|
||||||
|
|
||||||
# Delete memory units (cascades to unit_entities, memory_links)
|
# 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)
|
# Delete entities (cascades to unit_entities, entity_cooccurrences, memory_links with entity_id)
|
||||||
cursor.execute("DELETE FROM entities WHERE agent_id = %s", (agent_id,))
|
await conn.execute("DELETE FROM entities WHERE agent_id = $1", agent_id)
|
||||||
|
|
||||||
self.conn.commit()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"memory_units_deleted": units_count,
|
"memory_units_deleted": units_count,
|
||||||
|
|
@ -903,64 +781,11 @@ class TemporalSemanticMemory:
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.conn.rollback()
|
|
||||||
raise Exception(f"Failed to delete agent data: {str(e)}")
|
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]]:
|
async def _extract_entities_batch_optimized(
|
||||||
"""
|
|
||||||
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(
|
|
||||||
self,
|
self,
|
||||||
cursor,
|
conn,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
unit_ids: List[str],
|
unit_ids: List[str],
|
||||||
sentences: List[str],
|
sentences: List[str],
|
||||||
|
|
@ -1024,11 +849,12 @@ class TemporalSemanticMemory:
|
||||||
indices = [idx for idx, _ in entities_group]
|
indices = [idx for idx, _ in entities_group]
|
||||||
entities_data = [entity_data for _, entity_data 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,
|
agent_id=agent_id,
|
||||||
entities_data=entities_data,
|
entities_data=entities_data,
|
||||||
context=context,
|
context=context,
|
||||||
unit_event_date=fact_date
|
unit_event_date=fact_date,
|
||||||
|
conn=conn
|
||||||
)
|
)
|
||||||
|
|
||||||
for idx, entity_id in zip(indices, batch_resolved):
|
for idx, entity_id in zip(indices, batch_resolved):
|
||||||
|
|
@ -1049,7 +875,7 @@ class TemporalSemanticMemory:
|
||||||
unit_entity_pairs.append((unit_id, entity_id))
|
unit_entity_pairs.append((unit_id, entity_id))
|
||||||
|
|
||||||
# Batch insert all unit-entity links (MUCH faster!)
|
# 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.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")
|
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)
|
# For each entity, find all units that reference it (one query per entity)
|
||||||
entity_to_units = {}
|
entity_to_units = {}
|
||||||
for entity_id in all_entity_ids:
|
for entity_id in all_entity_ids:
|
||||||
cursor.execute(
|
rows = await conn.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT unit_id
|
SELECT unit_id
|
||||||
FROM unit_entities
|
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
|
# Create bidirectional links between units that share entities
|
||||||
links = []
|
links = []
|
||||||
|
|
@ -1098,71 +924,9 @@ class TemporalSemanticMemory:
|
||||||
# Re-raise to trigger rollback at put_async level
|
# Re-raise to trigger rollback at put_async level
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _create_temporal_links_batch(
|
async def _create_temporal_links_batch_per_fact(
|
||||||
self,
|
self,
|
||||||
cursor,
|
conn,
|
||||||
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,
|
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
unit_ids: List[str],
|
unit_ids: List[str],
|
||||||
time_window_hours: int = 24,
|
time_window_hours: int = 24,
|
||||||
|
|
@ -1177,55 +941,50 @@ class TemporalSemanticMemory:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from psycopg2.extras import execute_values
|
|
||||||
|
|
||||||
# Get the event_date for each new unit
|
# Get the event_date for each new unit
|
||||||
cursor.execute(
|
rows = await conn.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT id, event_date
|
SELECT id, event_date
|
||||||
FROM memory_units
|
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
|
# Create links based on each unit's individual event_date
|
||||||
links = []
|
links = []
|
||||||
for unit_id, unit_event_date in new_units.items():
|
for unit_id, unit_event_date in new_units.items():
|
||||||
# Find units within the time window of THIS specific unit
|
# Find units within the time window of THIS specific unit
|
||||||
cursor.execute(
|
recent_units = await conn.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT id, event_date
|
SELECT id, event_date
|
||||||
FROM memory_units
|
FROM memory_units
|
||||||
WHERE agent_id = %s
|
WHERE agent_id = $1
|
||||||
AND id != %s
|
AND id != $2
|
||||||
AND event_date BETWEEN %s AND %s
|
AND event_date BETWEEN $3 AND $4
|
||||||
ORDER BY event_date DESC
|
ORDER BY event_date DESC
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
""",
|
""",
|
||||||
(
|
|
||||||
agent_id,
|
agent_id,
|
||||||
unit_id,
|
unit_id,
|
||||||
unit_event_date - timedelta(hours=time_window_hours),
|
unit_event_date - timedelta(hours=time_window_hours),
|
||||||
unit_event_date + timedelta(hours=time_window_hours)
|
unit_event_date + timedelta(hours=time_window_hours)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
recent_units = cursor.fetchall()
|
for recent_row in recent_units:
|
||||||
|
recent_id = recent_row['id']
|
||||||
for recent_id, recent_event_date in recent_units:
|
recent_event_date = recent_row['event_date']
|
||||||
# Calculate temporal proximity weight
|
# Calculate temporal proximity weight
|
||||||
time_diff_hours = abs((unit_event_date - recent_event_date).total_seconds() / 3600)
|
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))
|
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:
|
if links:
|
||||||
execute_values(
|
await conn.executemany(
|
||||||
cursor,
|
|
||||||
"""
|
"""
|
||||||
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
|
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
|
ON CONFLICT (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid)) DO NOTHING
|
||||||
""",
|
""",
|
||||||
links
|
links
|
||||||
|
|
@ -1238,9 +997,9 @@ class TemporalSemanticMemory:
|
||||||
# Re-raise to trigger rollback at put_async level
|
# Re-raise to trigger rollback at put_async level
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _create_semantic_links_batch(
|
async def _create_semantic_links_batch(
|
||||||
self,
|
self,
|
||||||
cursor,
|
conn,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
unit_ids: List[str],
|
unit_ids: List[str],
|
||||||
embeddings: List[List[float]],
|
embeddings: List[List[float]],
|
||||||
|
|
@ -1256,37 +1015,36 @@ class TemporalSemanticMemory:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from psycopg2.extras import execute_values
|
|
||||||
|
|
||||||
all_links = []
|
all_links = []
|
||||||
|
|
||||||
for unit_id, embedding in zip(unit_ids, embeddings):
|
for unit_id, embedding in zip(unit_ids, embeddings):
|
||||||
# Find similar units using vector similarity
|
# 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
|
FROM memory_units
|
||||||
WHERE agent_id = %s
|
WHERE agent_id = $2
|
||||||
AND id != %s
|
AND id != $3
|
||||||
AND embedding IS NOT NULL
|
AND embedding IS NOT NULL
|
||||||
AND (1 - (embedding <=> %s::vector)) >= %s
|
AND (1 - (embedding <=> $1::vector)) >= $4
|
||||||
ORDER BY embedding <=> %s::vector
|
ORDER BY embedding <=> $1::vector
|
||||||
LIMIT %s
|
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 row in similar_units:
|
||||||
|
similar_id = row['id']
|
||||||
for similar_id, similarity in similar_units:
|
similarity = row['similarity']
|
||||||
all_links.append((unit_id, similar_id, 'semantic', float(similarity), None))
|
all_links.append((unit_id, str(similar_id), 'semantic', float(similarity), None))
|
||||||
|
|
||||||
if all_links:
|
if all_links:
|
||||||
execute_values(
|
await conn.executemany(
|
||||||
cursor,
|
|
||||||
"""
|
"""
|
||||||
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
|
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
|
ON CONFLICT (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid)) DO NOTHING
|
||||||
""",
|
""",
|
||||||
all_links
|
all_links
|
||||||
|
|
@ -1299,18 +1057,16 @@ class TemporalSemanticMemory:
|
||||||
# Re-raise to trigger rollback at put_async level
|
# Re-raise to trigger rollback at put_async level
|
||||||
raise
|
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."""
|
"""Insert all entity links in a single batch."""
|
||||||
if not links:
|
if not links:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from psycopg2.extras import execute_values
|
await conn.executemany(
|
||||||
execute_values(
|
|
||||||
cursor,
|
|
||||||
"""
|
"""
|
||||||
INSERT INTO memory_links (from_unit_id, to_unit_id, link_type, weight, entity_id)
|
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
|
ON CONFLICT (from_unit_id, to_unit_id, link_type, COALESCE(entity_id, '00000000-0000-0000-0000-000000000000'::uuid)) DO NOTHING
|
||||||
""",
|
""",
|
||||||
links
|
links
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@ description = "Temporal + Semantic + Entity Memory System for AI agents using Po
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"psycopg2-binary>=2.9.0",
|
"asyncpg>=0.29.0",
|
||||||
"pgvector>=0.2.0",
|
|
||||||
"python-dotenv>=1.0.0",
|
"python-dotenv>=1.0.0",
|
||||||
"openai>=1.0.0",
|
"openai>=1.0.0",
|
||||||
"pydantic>=2.0.0",
|
"pydantic>=2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ Pytest configuration and shared fixtures.
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
import os
|
import os
|
||||||
|
import asyncio
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from memory import TemporalSemanticMemory
|
from memory import TemporalSemanticMemory
|
||||||
import psycopg2
|
import asyncpg
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
@ -29,19 +30,19 @@ def clean_agent(memory):
|
||||||
agent_id = "test"
|
agent_id = "test"
|
||||||
|
|
||||||
# Clean up before test
|
# Clean up before test
|
||||||
memory.delete_agent(agent_id)
|
asyncio.run(memory.delete_agent(agent_id))
|
||||||
|
|
||||||
yield agent_id
|
yield agent_id
|
||||||
|
|
||||||
# Clean up after test
|
# Clean up after test
|
||||||
memory.delete_agent(agent_id)
|
asyncio.run(memory.delete_agent(agent_id))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def db_connection():
|
async def db_connection():
|
||||||
"""
|
"""
|
||||||
Provide a database connection for direct DB queries in tests.
|
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
|
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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "blis"
|
name = "blis"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|
@ -1009,13 +1041,12 @@ name = "memory-poc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "asyncpg" },
|
||||||
{ name = "langchain-text-splitters" },
|
{ name = "langchain-text-splitters" },
|
||||||
{ name = "matplotlib" },
|
{ name = "matplotlib" },
|
||||||
{ name = "networkx" },
|
{ name = "networkx" },
|
||||||
{ name = "nltk" },
|
{ name = "nltk" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "pgvector" },
|
|
||||||
{ name = "psycopg2-binary" },
|
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-asyncio" },
|
{ name = "pytest-asyncio" },
|
||||||
|
|
@ -1028,13 +1059,12 @@ dependencies = [
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "asyncpg", specifier = ">=0.29.0" },
|
||||||
{ name = "langchain-text-splitters", specifier = ">=0.3.0" },
|
{ name = "langchain-text-splitters", specifier = ">=0.3.0" },
|
||||||
{ name = "matplotlib", specifier = ">=3.7.0" },
|
{ name = "matplotlib", specifier = ">=3.7.0" },
|
||||||
{ name = "networkx", specifier = ">=3.0" },
|
{ name = "networkx", specifier = ">=3.0" },
|
||||||
{ name = "nltk", specifier = ">=3.8.0" },
|
{ name = "nltk", specifier = ">=3.8.0" },
|
||||||
{ name = "openai", specifier = ">=1.0.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 = "pydantic", specifier = ">=2.0.0" },
|
||||||
{ name = "pytest", specifier = ">=7.0.0" },
|
{ name = "pytest", specifier = ">=7.0.0" },
|
||||||
{ name = "pytest-asyncio", specifier = ">=0.21.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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "12.0.0"
|
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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.12.3"
|
version = "2.12.3"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue