File size: 5,729 Bytes
82eac1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Database Layer β€” PostgreSQL (asyncpg)
──────────────────────────────────────
ΰΉ€ΰΈΰΉ‡ΰΈš conversation history แΰΈ₯ΰΈ° sentiment results
"""

import logging
import asyncpg
from datetime import datetime, timezone
from app.config import settings

logger = logging.getLogger(__name__)


CREATE_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS conversations (
    id            SERIAL PRIMARY KEY,
    conv_id       TEXT        NOT NULL UNIQUE,
    user_id       TEXT        NOT NULL,
    message       TEXT        NOT NULL,
    source_type   TEXT        DEFAULT 'user',
    timestamp     BIGINT      NOT NULL,          -- LINE timestamp (ms)
    created_at    TIMESTAMPTZ DEFAULT NOW(),

    -- Sentiment fields (nullable ΰΈΰΉˆΰΈ­ΰΈ™ analyze ΰΉ€ΰΈͺΰΈ£ΰΉ‡ΰΈˆ)
    sentiment_label  TEXT,                        -- positive | neutral | negative
    sentiment_score  FLOAT,                       -- confidence score 0–1
    raw_scores       JSONB                         -- {"positive": 0.8, ...}
);

CREATE INDEX IF NOT EXISTS idx_conv_user     ON conversations(user_id);
CREATE INDEX IF NOT EXISTS idx_conv_created  ON conversations(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_conv_sentiment ON conversations(sentiment_label);
"""


class Database:
    def __init__(self):
        self._pool: asyncpg.Pool | None = None

    async def connect(self):
        self._pool = await asyncpg.create_pool(
            dsn=settings.DATABASE_URL,
            min_size=2,
            max_size=10,
        )
        logger.info("βœ… Database pool created")

    async def disconnect(self):
        if self._pool:
            await self._pool.close()

    async def _pool_ok(self) -> asyncpg.Pool:
        if not self._pool:
            await self.connect()
        return self._pool

    # ── Write ──────────────────────────────────────────────────────────────────

    async def save_conversation(
        self,
        conv_id: str,
        user_id: str,
        message: str,
        timestamp: int,
        source_type: str = "user",
    ) -> dict:
        pool = await self._pool_ok()
        async with pool.acquire() as conn:
            row = await conn.fetchrow(
                """
                INSERT INTO conversations (conv_id, user_id, message, source_type, timestamp)
                VALUES ($1, $2, $3, $4, $5)
                ON CONFLICT (conv_id) DO NOTHING
                RETURNING *
                """,
                conv_id, user_id, message, source_type, timestamp,
            )
        return dict(row) if row else {}

    async def update_sentiment(
        self,
        conv_id: str,
        label: str,
        score: float,
        raw_scores: dict,
    ) -> None:
        import json
        pool = await self._pool_ok()
        async with pool.acquire() as conn:
            await conn.execute(
                """
                UPDATE conversations
                SET sentiment_label = $1,
                    sentiment_score = $2,
                    raw_scores      = $3
                WHERE conv_id = $4
                """,
                label, score, json.dumps(raw_scores), conv_id,
            )

    # ── Read ───────────────────────────────────────────────────────────────────

    async def get_conversations(self, limit: int = 50, offset: int = 0) -> list[dict]:
        pool = await self._pool_ok()
        async with pool.acquire() as conn:
            rows = await conn.fetch(
                """
                SELECT conv_id, user_id, message, sentiment_label,
                       sentiment_score, raw_scores, created_at
                FROM conversations
                ORDER BY created_at DESC
                LIMIT $1 OFFSET $2
                """,
                limit, offset,
            )
        return [dict(r) for r in rows]

    async def get_sentiment_summary(self) -> dict:
        """ΰΈͺΰΈ£ΰΈΈΰΈ›ΰΈ’ΰΈ­ΰΈ”ΰΈ£ΰΈ§ΰΈ‘ sentiment ΰΈͺำหรับ stat cards"""
        pool = await self._pool_ok()
        async with pool.acquire() as conn:
            rows = await conn.fetch(
                """
                SELECT
                    COUNT(*) FILTER (WHERE sentiment_label = 'positive') AS positive,
                    COUNT(*) FILTER (WHERE sentiment_label = 'neutral')  AS neutral,
                    COUNT(*) FILTER (WHERE sentiment_label = 'negative') AS negative,
                    COUNT(*) AS total
                FROM conversations
                WHERE created_at >= NOW() - INTERVAL '7 days'
                """
            )
        row = dict(rows[0])
        total = row["total"] or 1  # ป้องกัน div/0
        return {
            "positive": {"count": row["positive"], "pct": round(row["positive"] / total * 100, 1)},
            "neutral":  {"count": row["neutral"],  "pct": round(row["neutral"]  / total * 100, 1)},
            "negative": {"count": row["negative"], "pct": round(row["negative"] / total * 100, 1)},
            "total":    row["total"],
            "period":   "last 7 days",
        }


async def init_db():
    """ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ table ΰΈ–ΰΉ‰ΰΈ²ΰΈ’ΰΈ±ΰΈ‡ΰΉ„ΰΈ‘ΰΉˆΰΈ‘ΰΈ΅ (ΰΈ£ΰΈ±ΰΈ™ΰΈ•ΰΈ­ΰΈ™ startup)"""
    pool = await asyncpg.create_pool(dsn=settings.DATABASE_URL)
    async with pool.acquire() as conn:
        await conn.execute(CREATE_TABLE_SQL)
    await pool.close()
    logger.info("βœ… Database initialized")


# Singleton
db = Database()