File size: 6,182 Bytes
ee76e96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from typing import Dict, Any, Optional
import logging

from ilm_athens_engine.deepseek_integration.deepseek_runner import DeepSeekR1Engine

logger = logging.getLogger(__name__)


# ドメイン別のプロンプトテンプレート
DOMAIN_PROMPT_TEMPLATES = {
    "medical": """【医療知識ベース参考情報】
{db_context}

【セッション履歴】
{session_summary}

【ユーザーの医療質問】
{question}

上記の参考情報を踏まえ、医学的に正確な回答を構造化フォーマットで提供してください。""",

    "legal": """【法律知識ベース参考情報】
{db_context}

【セッション履歴】
{session_summary}

【ユーザーの法律質問】
{question}

上記の参考情報を踏まえ、法的に正確な回答を構造化フォーマットで提供してください。
※これは法的助言ではなく、情報提供です。""",

    "economics": """【経済知識ベース参考情報】
{db_context}

【セッション履歴】
{session_summary}

【ユーザーの経済質問】
{question}

上記の参考情報を踏まえ、客観的な経済分析を構造化フォーマットで提供してください。""",

    "general": """【参考情報】
{db_context}

【ユーザーの質問】
{question}"""
}


class AlpheLobe:
    """
    生成院(α-Lobe):DeepSeek R1エンジンをラップし、
    ドメイン対応の構造化されたプロンプトを渡して回答を生成する責務を持つ。
    """
    def __init__(self, engine: DeepSeekR1Engine):
        self.engine = engine

    def _format_db_context(self, db_context: Dict) -> str:
        """DB知識コンテキストを読みやすい形式にフォーマット"""
        if not db_context:
            return "(参考情報なし)"

        formatted_parts = []
        for coord, tile in db_context.items():
            if tile:
                content = tile.get("content", tile.get("data", ""))
                if isinstance(content, str) and content:
                    formatted_parts.append(f"[{coord}]\n{content[:500]}")

        return "\n\n".join(formatted_parts) if formatted_parts else "(参考情報なし)"

    def _format_session_context(self, session_context: Optional[Dict]) -> str:
        """セッション履歴を要約"""
        if not session_context:
            return "(新規セッション)"

        history = session_context.get("history", [])
        if not history:
            return "(履歴なし)"

        # 直近3件の会話を要約
        recent = history[-3:]
        summary_parts = []
        for item in recent:
            q = item.get("question", "")[:50]
            summary_parts.append(f"Q: {q}...")

        return "\n".join(summary_parts)

    async def generate_response(
        self,
        question: str,
        db_context: Dict,
        session_context: Optional[Dict] = None,
        domain_id: str = "medical"
    ) -> Dict[str, Any]:
        """
        ドメイン対応のプロンプトを構築し、DeepSeekエンジンで推論を実行する。
        """
        # 1. プロンプトテンプレートを選択
        template = DOMAIN_PROMPT_TEMPLATES.get(domain_id, DOMAIN_PROMPT_TEMPLATES["general"])

        # 2. コンテキストをフォーマット
        formatted_db = self._format_db_context(db_context)
        formatted_session = self._format_session_context(session_context)

        # 3. プロンプトを構築
        prompt = template.format(
            db_context=formatted_db,
            session_summary=formatted_session,
            question=question
        )

        logger.debug(f"AlpheLobe生成プロンプト (domain={domain_id}): {prompt[:200]}...")

        # 4. DeepSeekエンジンに推論を依頼
        result = await self.engine.infer(
            prompt,
            domain_context={"domain_name": domain_id}
        )

        # 5. 不確実性を抽出
        uncertainties = self._extract_uncertainties(result.get("response", ""))

        # 6. Judge層が期待する形式に合わせる
        return {
            "main_response": result.get("response", ""),
            "thinking_process": result.get("thinking", ""),
            "structured": result.get("structured", {}),
            "confidence": result.get("confidence", 0.0),
            "domain": domain_id,
            "sources_cited": self._extract_sources(result.get("response", "")),
            "uncertainties": uncertainties,
            "latency_ms": result.get("latency_ms", 0)
        }

    def _extract_uncertainties(self, response: str) -> list:
        """回答から不確実性の言及を抽出"""
        uncertainty_markers = [
            "可能性があります",
            "かもしれません",
            "不確実",
            "データが限定的",
            "議論があります",
            "見解が分かれ",
            "要検討",
            "専門家に相談"
        ]

        uncertainties = []
        for marker in uncertainty_markers:
            if marker in response:
                # マーカー周辺のコンテキストを抽出
                idx = response.find(marker)
                start = max(0, idx - 30)
                end = min(len(response), idx + len(marker) + 30)
                context = response[start:end]
                uncertainties.append({
                    "marker": marker,
                    "context": context.strip()
                })

        return uncertainties[:5]  # 最大5件

    def _extract_sources(self, response: str) -> list:
        """回答から参考文献の言及を抽出"""
        import re

        sources = []

        # 【参考】セクションを探す
        ref_match = re.search(r'【参考[^】]*】(.+?)(?=【|$)', response, re.DOTALL)
        if ref_match:
            ref_text = ref_match.group(1).strip()
            # 行ごとに分割して抽出
            for line in ref_text.split('\n'):
                line = line.strip()
                if line and len(line) > 5:
                    sources.append(line[:100])

        return sources[:5]  # 最大5件