Spaces:
Running
Running
File size: 5,017 Bytes
473bd03 1a61e27 24480a0 473bd03 24480a0 473bd03 24480a0 473bd03 24480a0 473bd03 24480a0 473bd03 24480a0 473bd03 24480a0 473bd03 24480a0 473bd03 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | """backend/api/agent_memory.py β Agent memory CRUD (S354).
GAP-MEM-FIX: aggiunta riconciliazione _mem_fallback β Supabase.
Problema confermato: quando Supabase Γ¨ temporaneamente offline, le voci
finiscono solo in _mem_fallback (dict in-process). Al restart del backend
(HF Space free-tier riavvia spesso) il fallback viene perso completamente.
Fix: dopo ogni write Supabase riuscita, schedula un tentativo di sync del
fallback β se ci sono voci orfane le pubblica su Supabase e le rimuove dal
fallback locale. Nessun job periodico (troppo pesante su free-tier) β lazy
reconciliation al primo write riuscito dopo un periodo di downtime Supabase.
"""
import time, asyncio
from fastapi import APIRouter, Depends
from .auth_guard import require_role, AuthRole
from pydantic import BaseModel
from .state import _sb, _mem_fallback
import logging
_logger = logging.getLogger("api.agent_memory")
router = APIRouter( dependencies=[Depends(require_role(AuthRole.MACHINE))]) # GAP-1-fix: router-level auth
class MemoryEntry(BaseModel):
key: str
value: str
category: str = 'general'
createdAt: int = 0
updatedAt: int = 0
async def _reconcile_fallback() -> int:
"""GAP-MEM-FIX: sincronizza voci _mem_fallback β Supabase.
Chiama dopo ogni write Supabase riuscita: se ci sono voci scritte
solo in fallback (es. dopo un periodo di downtime Supabase), le pubblica.
Ritorna il numero di voci sincronizzate.
Non solleva mai eccezioni β fire-and-forget.
"""
if not _sb or not _mem_fallback:
return 0
synced = 0
for key, entry in list(_mem_fallback.items()):
try:
_sb.table('agent_memory').upsert({
'key': entry['key'],
'value': entry['value'],
'category': entry.get('category', 'general'),
'created_at': entry.get('createdAt', 0),
'updated_at': entry.get('updatedAt', 0),
}, on_conflict='key').execute()
synced += 1
except Exception as _e:
_logger.debug("[memory] reconcile stopped at key=%s: %s", key, _e)
break # Supabase non disponibile β interrompi, riprova al prossimo write
if synced:
_logger.info("[memory] GAP-MEM-FIX: reconciled %d fallback entries to Supabase", synced)
return synced
@router.get('/api/memory/agent')
async def list_agent_memory():
if _sb:
try:
data = _sb.table('agent_memory').select('*').order('updated_at', desc=True).limit(500).execute() # BUGFIX: LIMIT 500 β senza limit OOM su account grandi
entries = [
{'key': r['key'], 'value': r['value'], 'category': r.get('category', 'general'),
'createdAt': r.get('created_at', 0), 'updatedAt': r.get('updated_at', 0)}
for r in (data.data or [])
]
return {'entries': entries}
except Exception as e:
_logger.warning('[memory] Supabase list error: %s', e)
return {'entries': list(_mem_fallback.values())}
@router.get('/api/memory/agent/{key}')
async def get_agent_memory(key: str):
if _sb:
try:
data = _sb.table('agent_memory').select('*').eq('key', key).limit(1).execute()
if data.data:
return {'value': data.data[0]['value']}
except Exception as e:
_logger.warning('[memory] Supabase get error: %s', e)
entry = _mem_fallback.get(key)
return {'value': entry['value'] if entry else None}
@router.post('/api/memory/agent')
async def set_agent_memory(entry: MemoryEntry):
now = int(time.time() * 1000)
record = {
'key': entry.key, 'value': entry.value, 'category': entry.category,
'createdAt': entry.createdAt or now, 'updatedAt': entry.updatedAt or now,
}
# Sempre scrivi in fallback prima (garanzia immediata)
_mem_fallback[entry.key] = record
if _sb:
try:
_sb.table('agent_memory').upsert({
'key': entry.key, 'value': entry.value, 'category': entry.category,
'created_at': entry.createdAt or now, 'updated_at': entry.updatedAt or now,
}, on_conflict='key').execute()
# GAP-MEM-FIX: Supabase disponibile β schedula riconciliazione fallback orfano
# (voci scritte solo in fallback durante downtime precedente)
if len(_mem_fallback) > 1:
asyncio.create_task(_reconcile_fallback())
except Exception as _e:
_logger.warning('[memory] Supabase write error (fallback attivo): %s', _e)
return {'ok': True, 'key': entry.key}
@router.delete('/api/memory/agent/{key}')
async def delete_agent_memory(key: str):
if _sb:
try:
_sb.table('agent_memory').delete().eq('key', key).execute()
except Exception as _exc:
_logger.debug("[agent_memory] silenced %s", type(_exc).__name__) # noqa: BLE001
_mem_fallback.pop(key, None)
return {'deleted': key}
|