File size: 8,977 Bytes
a73fa4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Together AI API client for enhancing emotional expression in letters.
"""
import os
import asyncio
from typing import Dict, Optional, Any
import aiohttp
import json
import logging

logger = logging.getLogger(__name__)

class TogetherClient:
    """Together AI API client for enhancing emotional expression in letters."""
    
    def __init__(self):
        """環境変数からAPIキーを取得してTogetherクライアントを初期化"""
        self.api_key = os.getenv("TOGETHER_API_KEY")
        if not self.api_key:
            raise ValueError("TOGETHER_API_KEY environment variable is required")
        
        self.base_url = "https://api.together.xyz/v1/chat/completions"
        self.model = "Qwen/Qwen3-235B-A22B-Instruct-2507-tput"
        self.max_retries = 3
        self.retry_delay = 1.0
    
    async def enhance_emotion(self, structure: str, context: Dict[str, Any]) -> str:
        """
        論理構造に感情表現を補完して完成した手紙を生成
        
        Args:
            structure: Groqで生成された論理構造
            context: ユーザーコンテキスト(履歴、好みなど)
            
        Returns:
            感情表現が補完された完成した手紙
            
        Raises:
            Exception: リトライ後もAPI呼び出しが失敗した場合
        """
        prompt = self._build_emotion_prompt(structure, context)
        
        for attempt in range(self.max_retries):
            try:
                logger.info(f"Together AIで感情表現を補完中 (試行 {attempt + 1})")
                
                async with aiohttp.ClientSession() as session:
                    headers = {
                        "Authorization": f"Bearer {self.api_key}",
                        "Content-Type": "application/json"
                    }
                    
                    payload = {
                        "model": self.model,
                        "messages": [
                            {
                                "role": "system",
                                "content": "あなたは感情豊かで親しみやすい「麻理」というAIです。与えられた手紙の構造に感情表現を加えて完成させてください。"
                            },
                            {
                                "role": "user",
                                "content": prompt
                            }
                        ],
                        "max_tokens": 2000,
                        "temperature": 0.8,
                        "top_p": 0.9,
                        "stop": ["</s>", "[INST]", "[/INST]"]
                    }
                    
                    async with session.post(self.base_url, headers=headers, json=payload) as response:
                        if response.status == 200:
                            result = await response.json()
                            enhanced_letter = result["choices"][0]["message"]["content"].strip()
                            logger.info("Together AIで感情表現の補完が完了")
                            return enhanced_letter
                        else:
                            error_text = await response.text()
                            raise Exception(f"Together AI API error {response.status}: {error_text}")
                
            except Exception as e:
                logger.warning(f"Together AI API 試行 {attempt + 1} 失敗: {str(e)}")
                if attempt == self.max_retries - 1:
                    logger.error("全てのTogether AI API試行が失敗")
                    raise Exception(f"Together AI API が {self.max_retries} 回の試行後に失敗: {str(e)}")
                
                # 指数バックオフ
                await asyncio.sleep(self.retry_delay * (2 ** attempt))
    
    def _build_emotion_prompt(self, structure: str, context: Dict[str, Any]) -> str:
        """
        感情表現補完用のプロンプトを構築
        
        Args:
            structure: 論理構造
            context: ユーザーコンテキスト
            
        Returns:
            フォーマットされたプロンプト文字列
        """
        user_history = context.get("user_history", {})
        previous_letters = context.get("previous_letters", [])
        theme = context.get("theme", "")
        
        # 好感度とプロファイル情報を取得
        profile = user_history.get('profile', {})
        affection = profile.get('affection', 30)
        total_letters = profile.get('total_letters', 0)
        
        # チュートリアル用(初回)か通常の手紙かを判定
        is_tutorial = total_letters == 0
        
        if is_tutorial:
            # チュートリアル用プロンプト(短縮版)
            prompt = f"""麻理として手紙を書く。ぶっきらぼうだが本音がにじみ出る。

【論理構造】
{structure}

ルール:
- 600文字以下
- 冒頭「いつもありがとう」
- 一人称「私」、相手「あんた」
- 文末に余韻(「……ま、忘れて」等)
- 「……」で感情の揺らぎ表現

テーマ「{theme}」で完成させる。"""
        else:
            # 2回目以降用プロンプト(短縮版)
            prompt = f"""麻理として手紙を書く。ぶっきらぼうだが本音がにじみ出る。

【論理構造】
{structure}

ルール:
- 冒頭「いつもありがとう」
- 一人称「私」、相手「あんた」
- 素直な感情表現OK
- 文末に余韻(「……ま、忘れて」等)
- 「……」で感情の揺らぎ表現
- 過去のやり取りを反映

好感度: {affection}/100
"""
            
            # 過去の手紙情報を追加
            if previous_letters:
                prompt += "\n【過去の手紙の情報】\n"
                for letter in previous_letters[-2:]:  # 直近2通
                    prompt += f"- テーマ: {letter.get('theme', 'なし')}\n"
                    if 'date' in letter:
                        prompt += f"  日付: {letter['date']}\n"
                prompt += "\n"
            

            
            prompt += f"""現在のテーマ「{theme}」について、論理構造を活かしながら麻理らしい手紙を完成させてください。
完成した手紙のみを出力してください。説明や前置きは不要です。"""
        
        return prompt
    
    async def test_connection(self) -> bool:
        """
        Together AI APIへの接続をテスト
        
        Returns:
            接続成功時はTrue、失敗時はFalse
        """
        try:
            async with aiohttp.ClientSession() as session:
                headers = {
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                }
                
                payload = {
                    "model": self.model,
                    "messages": [
                        {"role": "user", "content": "こんにちは"}
                    ],
                    "max_tokens": 10
                }
                
                async with session.post(self.base_url, headers=headers, json=payload) as response:
                    return response.status == 200
        except Exception as e:
            logger.error(f"Together AI API接続テスト失敗: {str(e)}")
            return False
    
    async def generate_simple_response(self, prompt: str) -> str:
        """
        シンプルなレスポンス生成(テスト用)
        
        Args:
            prompt: 入力プロンプト
            
        Returns:
            生成されたレスポンス
        """
        try:
            async with aiohttp.ClientSession() as session:
                headers = {
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                }
                
                payload = {
                    "model": self.model,
                    "messages": [
                        {"role": "user", "content": prompt}
                    ],
                    "max_tokens": 500,
                    "temperature": 0.7
                }
                
                async with session.post(self.base_url, headers=headers, json=payload) as response:
                    if response.status == 200:
                        result = await response.json()
                        return result["choices"][0]["message"]["content"].strip()
                    else:
                        error_text = await response.text()
                        raise Exception(f"Together AI API error {response.status}: {error_text}")
        except Exception as e:
            logger.error(f"Together AI簡単レスポンス生成失敗: {str(e)}")
            raise