Terminal / api /agent_memory.py
Baida07's picture
Update api/agent_memory.py
9a303bd verified
Raw
History Blame
4.85 kB
"""
backend/api/agent_memory.py — Agent memory CRUD (S354).
GAP-MEM-FIX: aggiunta riconciliazione _mem_fallback → Supabase.
GAP-SENSITIVE-FIX: implementato masking per le chiavi definite in SENSITIVE.
"""
import time, asyncio
from typing import Any
from fastapi import APIRouter, Depends
from .auth_guard import require_role, AuthRole
from pydantic import BaseModel
from .state import _sb, _mem_fallback, SENSITIVE
import logging
_logger = logging.getLogger("api.agent_memory")
# Router protetto a livello MACHINE — richiede X-Internal-Token
router = APIRouter(dependencies=[Depends(require_role(AuthRole.MACHINE))])
class MemoryEntry(BaseModel):
key: str
value: str
category: str = 'general'
createdAt: int = 0
updatedAt: int = 0
def _mask_value(key: str, value: Any) -> Any:
"""Maschera il valore se la chiave è presente nel set SENSITIVE."""
if key in SENSITIVE and value:
return "[REDACTED]"
return value
async def _reconcile_fallback() -> int:
"""GAP-MEM-FIX: sincronizza voci _mem_fallback → Supabase."""
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
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():
"""Lista le voci di memoria, mascherando i segreti."""
if _sb:
try:
data = _sb.table('agent_memory').select('*').order('updated_at', desc=True).limit(500).execute()
entries = [
{
'key': r['key'],
'value': _mask_value(r['key'], 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)
entries = [
{
'key': v['key'],
'value': _mask_value(v['key'], v['value']),
'category': v.get('category', 'general'),
'createdAt': v.get('createdAt', 0),
'updatedAt': v.get('updatedAt', 0)
}
for v in _mem_fallback.values()
]
return {'entries': entries}
@router.get('/api/memory/agent/{key}')
async def get_agent_memory(key: str):
"""Recupera una singola voce di memoria, mascherando se sensibile."""
val = None
if _sb:
try:
data = _sb.table('agent_memory').select('*').eq('key', key).limit(1).execute()
if data.data:
val = data.data[0]['value']
except Exception as e:
_logger.warning('[memory] Supabase get error: %s', e)
if val is None:
entry = _mem_fallback.get(key)
val = entry['value'] if entry else None
return {'value': _mask_value(key, val)}
@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,
}
_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()
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__)
_mem_fallback.pop(key, None)
return {'deleted': key}