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}