File size: 10,098 Bytes
8ce09fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import mlx.core as mx
from mlx_lm import load, generate
from sentence_transformers import SentenceTransformer, util
from transformers import pipeline
from collections import deque
from janome.tokenizer import Tokenizer
import json
import sqlite3
from datetime import datetime

class IlmApp:
    # --- 定数: プロンプトテンプレート ---
    BASE_PROMPT = """### 指示:
あなたは、文脈を理解し、自然な応答を生成するAIアシスタントです。
以下の状況を考慮して、最適な応答を生成してください。
{intent_instruction}
{style_instruction}
{transition_instruction}
### ユーザーからの入力:
{user_input}

### 応答:"""
    FLASHBACK_PROMPT = """... (省略) ..."""
    TRANSITION_CONTEXT = """### 直前の会話のトピック:
{previous_response}
"""

    # --- 定数: 意図・スタイルに応じた指示 ---
    INTENT_INSTRUCTIONS = {"質問": "...", "アイデアの要求": "...", "感想": "...", "雑談": "...", "デフォルト": "..."}
    STYLE_INSTRUCTIONS = {"丁寧": "...", "簡潔": "...", "創造的": "...", "ユーモラス": "...", "デフォルト": "..."}

    def __init__(self):
        # --- 設定 ---
        self.db_path = "experience.db"
        self.llm_model_path = "./merged_model"
        self.sentence_model_name = "paraphrase-multilingual-MiniLM-L12-v2"
        self.classifier_name = "MoritzLaurer/mDeBERTa-v3-base-mnli-xnli"
        self.intent_labels = ["質問", "アイデアの要求", "感想", "雑談"]
        self.style_labels = ["丁寧", "簡潔", "創造的", "ユーモラス"]
        self.topic_labels = ["テクノロジー", "ビジネス", "健康", "芸術", "食事", "地理", "歴史", "科学"]
        self.similarity_threshold = 0.4
        self.common_transition_threshold = 5 # 5回以上経験したら「自然な遷移」と判断

        # --- 状態管理 ---
        self.flashback_buffer = deque(maxlen=5)
        self.previous_response = None

        # --- 初期化 ---
        self._init_db()
        self._load_models()

    def _init_db(self):
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("""
            CREATE TABLE IF NOT EXISTS topic_transitions (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                source_topic TEXT NOT NULL,
                destination_topic TEXT NOT NULL,
                count INTEGER NOT NULL DEFAULT 1,
                last_occurred TIMESTAMP NOT NULL,
                UNIQUE(source_topic, destination_topic)
            )
            """)
            conn.commit()

    def _load_models(self):
        print("各モデルとツールを読み込んでいます...")
        self.model, self.tokenizer = load(self.llm_model_path)
        self.sentence_model = SentenceTransformer(self.sentence_model_name)
        self.classifier = pipeline("zero-shot-classification", model=self.classifier_name)
        self.janome_tokenizer = Tokenizer()
        print("モデルの読み込みが完了しました。")

    def _get_transition_experience(self, source, dest):
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT count FROM topic_transitions WHERE source_topic = ? AND destination_topic = ?", (source, dest))
            result = cursor.fetchone()
            return result[0] if result else 0

    def _update_l2_memory(self, source, dest):
        timestamp = datetime.now().isoformat()
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("""
            INSERT INTO topic_transitions (source_topic, destination_topic, last_occurred) VALUES (?, ?, ?)
            ON CONFLICT(source_topic, destination_topic) DO UPDATE SET count = count + 1, last_occurred = excluded.last_occurred
            """, (source, dest, timestamp))
            conn.commit()

    def _build_prompt(self, user_input):
        # 1. フラッシュバック検知
        for item in self.flashback_buffer:
            if item['keyword'] in user_input:
                print(f"(フラッシュバックを検知: {item['keyword']})", end="")
                return self.FLASHBACK_PROMPT.format(keyword=item['keyword'], original_sentence=item['sentence'], user_input=user_input)

        # 2. 意図とスタイルの分析
        intent = self.classifier(user_input, self.intent_labels, multi_label=False)['labels'][0]
        style = self.classifier(user_input, self.style_labels, multi_label=False)['labels'][0]
        print(f"(意図: {intent} | スタイル: {style})", end="")

        intent_instruction = f"\n### ユーザーの意図: {intent}\n{self.INTENT_INSTRUCTIONS.get(intent, self.INTENT_INSTRUCTIONS['デフォルト'])}"
        style_instruction = f"\n### ユーザーの対話スタイル: {style}\n{self.STYLE_INSTRUCTIONS.get(style, self.STYLE_INSTRUCTIONS['デフォルト'])}"
        transition_instruction = ""

        # 3. 話題遷移の検知とL2記憶の活用
        if self.previous_response:
            emb_prev = self.sentence_model.encode(self.previous_response, convert_to_tensor=True)
            emb_curr = self.sentence_model.encode(user_input, convert_to_tensor=True)
            sim = util.pytorch_cos_sim(emb_prev, emb_curr).item()
            print(f" (類似度: {sim:.2f})", end="")

            if sim < self.similarity_threshold:
                source_topic = self.classifier(self.previous_response, self.topic_labels, multi_label=False)['labels'][0]
                dest_topic = self.classifier(user_input, self.topic_labels, multi_label=False)['labels'][0]
                
                # L2記憶を参照して判断
                experience_count = self._get_transition_experience(source_topic, dest_topic)
                
                if experience_count > self.common_transition_threshold:
                    transition_judgment = f"これは過去に{experience_count}回経験した、自然な話題の遷移です。その流れを汲み取って応答してください。"
                else:
                    transition_judgment = f"これは斬新な話題の飛躍です。その面白さに触れつつ、応答を返してください。"
                
                print(f" (L2判断: {transition_judgment})")
                transition_instruction = f"\n### 話題遷移の分析:\n{transition_judgment}\nスムーズな移行文を生成してください。\n" + self.TRANSITION_CONTEXT.format(previous_response=self.previous_response)

                # L2記憶を更新
                if source_topic != dest_topic:
                    self._update_l2_memory(source_topic, dest_topic)
            else:
                print()
        
        return self.BASE_PROMPT.format(intent_instruction=intent_instruction, style_instruction=style_instruction, transition_instruction=transition_instruction, user_input=user_input)

    def _update_memory(self, response):
        self.previous_response = response
        try:
            for token in self.janome_tokenizer.tokenize(response):
                if token.part_of_speech.startswith('名詞'):
                    self.flashback_buffer.append({'keyword': token.surface, 'sentence': response}); break
        except Exception: pass

    def run(self):
        print("\nIlmチャットを開始します。終了するには 'exit' と入力してください。")
        while True:
            user_input = input("\nあなた: ")
            if user_input.lower() == 'exit': break
            prompt = self._build_prompt(user_input)
            print("\nIlm: ", end="", flush=True)
            current_response = ""
            for token in generate(self.model, self.tokenizer, prompt=prompt, verbose=False):
                current_response += token
                print(token, end="", flush=True)
            print()
            self._update_memory(current_response)

if __name__ == "__main__":
    # 定義が長くなったため、クラス外で定数を設定
    IlmApp.FLASHBACK_PROMPT = """### 指示:
あなたは、以前の会話の断片を思い出すことができます。
ユーザーが、あなたが以前言及したキーワード「{keyword}」に触れました。
あなたはそのキーワードについて「{original_sentence}」と発言しています。
この「記憶の断片」を思い出したかのように自然な前置きを述べてから、ユーザーの現在の入力に答えてください。

### ユーザーからの現在の入力:
{user_input}

### 応答:"""
    IlmApp.INTENT_INSTRUCTIONS = {
        "質問": "ユーザーは具体的な情報を求めています。明確かつ簡潔に回答してください。",
        "アイデアの要求": "ユーザーは創造的な発想を求めています。斬新で多様なアイデアを提案してください。",
        "感想": "ユーザーは共感を求めています。同意や補足情報を提供し、会話を広げてください。",
        "雑談": "ユーザーは気軽な対話を望んでいます。親しみやすいトーンで応答してください。",
        "デフォルト": "ユーザーの入力に対して、適切に応答してください。"
    }
    IlmApp.STYLE_INSTRUCTIONS = {
        "丁寧": "ユーザーは丁寧な言葉遣いを好みます。あなたも敬体(です・ます調)で応答してください。",
        "簡潔": "ユーザーは要点をまとめて話しています。あなたも簡潔に応答してください。",
        "創造的": "ユーザーは比喩や創造的な表現を使っています。あなたも表現を工夫して応答してください。",
        "ユーモラス": "ユーザーはユーモアを交えて話しています。あなたも遊び心のある応答をしてください。",
        "デフォルト": "ユーザーのスタイルに合わせて、自然に応答してください。"
    }
    app = IlmApp()
    app.run()