Spaces:
Running
Running
File size: 4,845 Bytes
b2b3de9 473bd03 b2b3de9 473bd03 8f60f3a 1a61e27 24480a0 473bd03 b2b3de9 1656cf0 473bd03 b2b3de9 1656cf0 b2b3de9 473bd03 b2b3de9 473bd03 b2b3de9 473bd03 24480a0 b2b3de9 473bd03 b2b3de9 473bd03 b2b3de9 473bd03 b2b3de9 473bd03 b2b3de9 473bd03 b2b3de9 473bd03 b2b3de9 473bd03 b2b3de9 473bd03 24480a0 473bd03 24480a0 473bd03 24480a0 473bd03 b2b3de9 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 126 127 128 129 130 131 132 133 | """
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}
|