File size: 2,427 Bytes
6a2306e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""

Pydantic models for Level Bridge Chat API.

Strict JSON Schema -- MCP-wrappable without modification.

"""

from __future__ import annotations
from typing import Optional
from pydantic import BaseModel, Field, field_validator
import base64


class Metrics(BaseModel):
    cvr: Optional[float] = Field(None, description="CVR (%)")
    ctr: Optional[float] = Field(None, description="CTR (%)")
    cpa: Optional[float] = Field(None, description="CPA (円)")

    def has_any(self) -> bool:
        return any(v is not None for v in [self.cvr, self.ctr, self.cpa])


class DashboardContext(BaseModel):
    campaign_name: Optional[str] = None
    industry: Optional[str] = None
    metrics: Optional[Metrics] = None
    image_base64: Optional[str] = None

    @field_validator("image_base64")
    @classmethod
    def validate_image_size(cls, v: Optional[str]) -> Optional[str]:
        if v is None:
            return v
        import os
        max_mb = float(os.environ.get("MAX_IMAGE_SIZE_MB", "5"))
        size_bytes = len(v.encode("utf-8")) * 3 / 4  # base64 -> bytes approx
        if size_bytes > max_mb * 1024 * 1024:
            raise ValueError(f"image_base64 exceeds {max_mb}MB limit")
        return v


class BridgeRequest(BaseModel):
    session_id: Optional[str] = None
    message: str = Field(default="", description="User message (can be empty on Turn 1)")
    dashboard_context: Optional[DashboardContext] = None


class NeededInfo(BaseModel):
    key: str
    label: str
    example: str


class BestNow(BaseModel):
    summary: str
    actions: list[str]
    confidence: str = Field(..., pattern="^(low|mid|high)$")
    reasoning_basis: list[str]


class NextLevelPreview(BaseModel):
    current_level: str
    next_level: Optional[str]
    needed_info: list[NeededInfo]
    what_will_be_possible: list[str]
    expected_impact: str


class BridgeResponse(BaseModel):
    ok: bool = True
    session_id: str
    turn_number: int
    inferred_level: str
    level_confidence: str = Field(..., pattern="^(low|mid|high)$")
    level_reason: str
    best_now: BestNow
    next_level_preview: NextLevelPreview
    follow_up_question: Optional[str] = None


class BridgeErrorResponse(BaseModel):
    ok: bool = False
    session_id: Optional[str] = None
    error_code: str
    message: str
    fallback: Optional[dict] = None