File size: 10,367 Bytes
ef78361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
"""Sentiment API methods for ChainShiftClient.

Mixin class extracted from api_client.py.
Methods access self._get, self._post, self._session, self.base_url, self.headers from the parent class.
"""
from typing import Any


class SentimentApiMixin:
    """Gen3 Nudge, Brand Mentions, LLM Verification, Feedback, Keyword APIs."""

    # ========================================================================
    # Gen3 APIs - Nudge Detection
    # ========================================================================

    def get_nudge_candidates(
        self,
        campaign_id: int,
        page: int = 1,
        page_size: int = 50,
        bit_quadrant: str | None = None,
        platform: str | None = None,
    ) -> dict:
        """Get answers flagged for nudge (in-house brand negative mentions).

        Returns:
            - total_nudge_candidates: int
            - by_confidence_tier: {HIGH: n, MEDIUM: n, LOW: n}
            - by_platform: {CHATGPT: n, GOOGLE_AI: n, ...}
            - by_cej: {AWARENESS_COMPARISON: n, PURCHASE: n, ...}
            - by_bit_quadrant: {neutral: n, ...}
            - candidates: list of nudge items
        """
        params = {"page": page, "page_size": page_size}
        if bit_quadrant:
            params["bit_quadrant"] = bit_quadrant
        if platform:
            params["platform"] = platform
        return self._get(f"/api/v1/sentiment/campaigns/{campaign_id}/nudge-candidates", params)

    def get_brand_mentions(
        self,
        campaign_id: int,
        page: int = 1,
        page_size: int = 50,
        brand_type: str | None = None,
        polarity: str | None = None,
        llm_verified: str | None = None,
        brand_name: str | None = None,
        sort_by: str | None = "analyzed_at",
        sort_desc: bool = True,
    ) -> dict:
        """Get brand mention analysis."""
        params = {"page": page, "page_size": page_size, "sort_desc": sort_desc}
        if brand_type:
            params["brand_type"] = brand_type
        if polarity:
            params["polarity"] = polarity
        if llm_verified:
            params["llm_verified"] = llm_verified
        if brand_name:
            params["brand_name"] = brand_name
        if sort_by:
            params["sort_by"] = sort_by
        return self._get(f"/api/v1/sentiment/campaigns/{campaign_id}/brand-mentions", params)

    def export_brand_mentions(
        self,
        campaign_id: int,
        brand_type: str | None = None,
        polarity: str | None = None,
        llm_verified: str | None = None,
        brand_name: str | None = None,
        include_full_answers: bool = False,
    ) -> bytes:
        """Export brand mentions to Excel."""
        params = {}
        if brand_type:
            params["brand_type"] = brand_type
        if polarity:
            params["polarity"] = polarity
        if llm_verified:
            params["llm_verified"] = llm_verified
        if brand_name:
            params["brand_name"] = brand_name
        if include_full_answers:
            params["include_full_answers"] = "true"

        url = f"{self.base_url}/api/v1/sentiment/campaigns/{campaign_id}/brand-mentions/export"
        response = self._session.get(url, params=params, headers=self.headers, timeout=120)
        response.raise_for_status()
        return response.content

    # ========================================================================
    # LLM Verification & Human Feedback APIs
    # ========================================================================

    def verify_answer(self, answer_id: int, force: bool = False) -> dict:
        """Request LLM 2차 검증 for an answer."""
        return self._post("/api/v1/sentiment/verify", {
            "answer_id": answer_id,
            "force": force,
        })

    def submit_feedback(
        self,
        answer_id: int,
        campaign_id: int,
        feedback_type: str,
        corrected_polarity: str | None = None,
        wrong_reason: str | None = None,
        comment: str | None = None,
    ) -> dict:
        """Submit human feedback for an answer sentiment."""
        data = {
            "answer_id": answer_id,
            "campaign_id": campaign_id,
            "feedback_type": feedback_type,
        }
        if corrected_polarity:
            data["corrected_polarity"] = corrected_polarity
        if wrong_reason:
            data["wrong_reason"] = wrong_reason
        if comment:
            data["comment"] = comment
        return self._post("/api/v1/sentiment/feedback", data)

    def get_feedback_stats(self, campaign_id: int) -> dict:
        """Get feedback statistics for a campaign."""
        return self._get(f"/api/v1/sentiment/campaigns/{campaign_id}/feedback/stats")

    def get_full_answers(self, answer_ids: list[int]) -> dict:
        """Get full answer text for multiple answers."""
        return self._post("/api/v1/sentiment/answers/full", {"answer_ids": answer_ids})

    def export_nudge_candidates(
        self,
        campaign_id: int,
        include_full_answers: bool = False,
        include_evidence: bool = True,
        llm_verified_only: bool = False,
        platform: str | None = None,
        llm_is_negative: bool | None = None,
    ) -> bytes:
        """Export nudge candidates to Excel."""
        url = f"{self.base_url}/api/v1/sentiment/campaigns/{campaign_id}/nudge-candidates/export"
        params = {
            "include_full_answers": str(include_full_answers).lower(),
            "include_evidence": str(include_evidence).lower(),
            "llm_verified_only": str(llm_verified_only).lower(),
        }
        if platform:
            params["platform"] = platform
        if llm_is_negative is not None:
            params["llm_is_negative"] = str(llm_is_negative).lower()
        response = self._session.get(url, headers=self.headers, params=params, timeout=120)
        response.raise_for_status()
        return response.content

    # ========================================================================
    # Keyword Sentiment APIs
    # ========================================================================

    def start_keyword_analysis(self, campaign_id: int, keywords: list[str]) -> dict:
        """Start a keyword sentiment analysis job."""
        return self._post(
            f"/api/v1/sentiment/campaigns/{campaign_id}/keyword-analysis",
            {"keywords": keywords},
        )

    def get_keyword_summary(
        self,
        campaign_id: int,
        keyword: str | None = None,
        page: int = 1,
        page_size: int = 50,
    ) -> dict:
        """Get keyword sentiment summary with optional filter and pagination."""
        params: dict[str, Any] = {"page": page, "page_size": page_size}
        if keyword:
            params["keyword"] = keyword
        return self._get(f"/api/v1/sentiment/campaigns/{campaign_id}/keyword-summary", params)

    def get_keyword_results(
        self,
        campaign_id: int,
        keyword: str | None = None,
        sentiment: str | None = None,
        brand_name: str | None = None,
        competitor: bool = False,
        llm_verified: str | None = None,
        llm_is_negative: bool | None = None,
        platform: str | None = None,
        brand_only: bool = False,
        no_brand: bool = False,
        page: int = 1,
        page_size: int = 50,
    ) -> dict:
        """Get keyword results with filters."""
        params: dict[str, Any] = {"page": page, "page_size": page_size}
        if keyword:
            params["keyword"] = keyword
        if sentiment:
            params["sentiment"] = sentiment
        if brand_name:
            params["brand_name"] = brand_name
        if competitor:
            params["competitor"] = "true"
        if llm_verified:
            params["llm_verified"] = llm_verified
        if llm_is_negative is not None:
            params["llm_is_negative"] = str(llm_is_negative).lower()
        if platform:
            params["platform"] = platform
        if brand_only:
            params["brand_only"] = "true"
        if no_brand:
            params["no_brand"] = "true"
        return self._get(
            f"/api/v1/sentiment/campaigns/{campaign_id}/keyword-results", params
        )

    def get_keyword_brand_analysis(
        self,
        campaign_id: int,
        keyword: str | None = None,
        brand_name: str | None = None,
        competitor: bool = False,
    ) -> dict:
        """Get keyword x brand cross analysis."""
        params: dict[str, Any] = {}
        if keyword:
            params["keyword"] = keyword
        if brand_name:
            params["brand_name"] = brand_name
        if competitor:
            params["competitor"] = "true"
        return self._get(
            f"/api/v1/sentiment/campaigns/{campaign_id}/keyword-brand-analysis", params
        )

    def export_keyword_results(
        self,
        campaign_id: int,
        keyword: str | None = None,
        sentiment: str | None = None,
        brand_name: str | None = None,
        competitor: bool = False,
        llm_verified: str | None = None,
        llm_is_negative: bool | None = None,
        platform: str | None = None,
        brand_only: bool = False,
        no_brand: bool = False,
        format: str = "xlsx",
    ) -> bytes:
        """Export keyword results to Excel or CSV with same filters as list endpoint."""
        params: dict[str, Any] = {"format": format}
        if keyword:
            params["keyword"] = keyword
        if sentiment:
            params["sentiment"] = sentiment
        if brand_name:
            params["brand_name"] = brand_name
        if competitor:
            params["competitor"] = "true"
        if llm_verified:
            params["llm_verified"] = llm_verified
        if llm_is_negative is not None:
            params["llm_is_negative"] = str(llm_is_negative).lower()
        if platform:
            params["platform"] = platform
        if brand_only:
            params["brand_only"] = "true"
        if no_brand:
            params["no_brand"] = "true"
        url = f"{self.base_url}/api/v1/sentiment/campaigns/{campaign_id}/keyword-results/export"
        response = self._session.get(url, params=params, headers=self.headers, timeout=120)
        response.raise_for_status()
        return response.content