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}