File size: 4,829 Bytes
79df050
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import logging
import json

import pytest

from core import memory_system


@pytest.mark.asyncio
async def test_memory_manager_skips_ai_analysis_for_transient_market_query(monkeypatch):
    class FailingAnalyzer:
        async def analyze_conversation(self, *args, **kwargs):
            raise AssertionError("AI memory analysis should be skipped for transient queries")

    manager = memory_system.MemoryManager()
    manager.analyzer = FailingAnalyzer()

    monkeypatch.setattr(memory_system, "_get_memory_client", lambda: object())
    monkeypatch.setattr(memory_system, "db_available", False)

    result = await manager.process_conversation(
        user_id="u1",
        user_message="今天台積電股價收盤價多少?",
        assistant_response="查詢中。",
        conversation_history=[],
    )

    assert result["extracted_memories"] == 0
    assert result["saved_memories"] == 0
    assert result["errors"] == []


@pytest.mark.asyncio
async def test_memory_analyzer_quietly_degrades_on_transient_upstream_error(monkeypatch, caplog):
    class Responses:
        def create(self, **kwargs):
            raise Exception("Error code: 502 - {'error': {'message': 'Upstream request failed'}}")

    class Client:
        responses = Responses()

    class Settings:
        OPENAI_USE_RESPONSES = True
        GPT_INTENT_MODEL = "gpt-5.4"
        OPENAI_MODEL = "gpt-5.4"
        OPENAI_TIMEOUT = 30

    monkeypatch.setattr(memory_system, "_get_memory_client", lambda: Client())
    monkeypatch.setattr(memory_system, "settings", Settings)

    analyzer = memory_system.MemoryAnalyzer()

    with caplog.at_level(logging.ERROR):
        memories = await analyzer.analyze_conversation(
            user_message="我喜歡安靜的咖啡店",
            assistant_response="我記住了。",
            conversation_history=[],
        )

    assert memories == []
    assert "AI記憶分析時發生錯誤" not in caplog.text


def test_memory_analyzer_uses_structured_responses_payload(monkeypatch):
    captured = {}

    class Responses:
        def create(self, **kwargs):
            captured.update(kwargs)

            class Response:
                output_text = '{"memories":[]}'

                output = []

            return Response()

    class Client:
        responses = Responses()

    class Settings:
        OPENAI_USE_RESPONSES = True
        GPT_INTENT_MODEL = "gpt-5.4-mini"
        OPENAI_MODEL = "gpt-5.4-mini"

    monkeypatch.setattr(memory_system, "settings", Settings)

    analyzer = memory_system.MemoryAnalyzer()
    response = analyzer._create_analysis_response(
        Client(),
        [{"role": "system", "content": "JSON"}, {"role": "user", "content": "JSON"}],
        500,
    )

    assert response.output_text == '{"memories":[]}'
    assert captured["model"] == "gpt-5.4-mini"
    assert captured["max_output_tokens"] == 500
    assert captured["store"] is False
    assert captured["text"]["format"]["type"] == "json_schema"
    assert captured["text"]["format"]["strict"] is True
    assert captured["text"]["format"]["schema"]["required"] == ["memories"]
    assert "reasoning" not in captured


@pytest.mark.asyncio
async def test_memory_analyzer_retries_transient_upstream_error_then_succeeds(monkeypatch):
    calls = {"count": 0}

    class Responses:
        def create(self, **kwargs):
            calls["count"] += 1
            if calls["count"] == 1:
                raise Exception("502 upstream request failed")

            class Response:
                output_text = json.dumps(
                    {
                        "memories": [
                            {
                                "type": "preferences",
                                "content": "使用者喜歡安靜的咖啡店",
                                "importance": 0.8,
                            }
                        ]
                    },
                    ensure_ascii=False,
                )

                output = []

            return Response()

    class Client:
        responses = Responses()

    class Settings:
        OPENAI_USE_RESPONSES = True
        GPT_INTENT_MODEL = "gpt-5.4-mini"
        OPENAI_MODEL = "gpt-5.4-mini"
        OPENAI_TIMEOUT = 30

    monkeypatch.setattr(memory_system, "_get_memory_client", lambda: Client())
    monkeypatch.setattr(memory_system, "settings", Settings)
    monkeypatch.setattr(memory_system.MemoryAnalyzer, "_transient_backoff", staticmethod(lambda attempt: _noop()))

    analyzer = memory_system.MemoryAnalyzer()
    memories = await analyzer.analyze_conversation("我喜歡安靜的咖啡店", "我記住了。", [])

    assert calls["count"] == 2
    assert memories[0]["type"] == "preferences"
    assert memories[0]["source"] == "ai_analysis"


async def _noop():
    return None