-
Notifications
You must be signed in to change notification settings - Fork 159
Expand file tree
/
Copy pathentry_tutor_explain_entries.py
More file actions
216 lines (208 loc) · 8.49 KB
/
entry_tutor_explain_entries.py
File metadata and controls
216 lines (208 loc) · 8.49 KB
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
from __future__ import annotations
from .entry_common import (
Any,
Err,
Ok,
SdkError,
_entry_exception_error,
_normalize_submitted_image_payload,
_validate_optional_vision_image_payload,
_plugin_lock,
plugin_entry,
tr,
LLM_OPERATION_CONCEPT_EXPLAIN,
MODE_COMPANION,
MODE_CONCEPT_EXPLAIN,
handle_user_intent,
)
IMAGE_ONLY_EXPLAIN_PROMPT_EN = "Please explain the pasted image."
IMAGE_ONLY_EXPLAIN_PROMPT_ZH_CN = "请查看这张图片的内容"
IMAGE_ONLY_EXPLAIN_PROMPT_ZH_TW = "請查看這張圖片的內容"
def _image_only_explain_prompt(language: str) -> str:
normalized = str(language or "").strip().lower()
if normalized.startswith(("zh-tw", "zh-hk", "zh-hant")):
return IMAGE_ONLY_EXPLAIN_PROMPT_ZH_TW
if normalized.startswith("zh"):
return IMAGE_ONLY_EXPLAIN_PROMPT_ZH_CN
return IMAGE_ONLY_EXPLAIN_PROMPT_EN
class _TutorExplainEntriesMixin:
@plugin_entry(
id="study_submit_image",
name=tr("entries.submit_image.name", default="Submit Study Image"),
description=tr(
"entries.submit_image.description",
default="Accept a user image and explain it with the configured vision model.",
),
input_schema={
"type": "object",
"properties": {
"image_base64": {"type": "string"},
"text": {"type": "string", "default": ""},
},
"required": ["image_base64"],
},
timeout=60.0,
llm_result_fields=["summary", "reply", "diagnostic"],
)
async def study_submit_image(self, image_base64: str, text: str = "", **_):
try:
image_payload = _normalize_submitted_image_payload(image_base64)
except ValueError as exc:
return _entry_exception_error(self, exc, operation="study_submit_image")
if not bool(self._cfg.llm_vision_enabled):
return Err(SdkError("llm_vision_enabled is not enabled"))
normalized_text = str(text or "").strip()
if normalized_text:
async with _plugin_lock(self._lock):
self._state.last_ocr_text = normalized_text
source_text = normalized_text or _image_only_explain_prompt(
self._cfg.language
)
return await self.study_explain_text(
text=source_text,
vision_image_base64=image_payload,
)
@plugin_entry(
id="study_explain_text",
name=tr("entries.explain_text.name", default="Explain Study Text"),
description=tr(
"entries.explain_text.description",
default="Explain a concept from supplied text, or use the latest OCR text if text is omitted.",
),
input_schema={
"type": "object",
"properties": {
"text": {"type": "string", "default": ""},
"vision_image_base64": {"type": "string", "default": ""},
},
},
timeout=45.0,
llm_result_fields=["summary", "reply", "diagnostic"],
)
async def study_explain_text(
self, text: str = "", vision_image_base64: str = "", **_
):
if self._agent is None:
return Err(SdkError("study tutor agent is not initialized"))
raw_text = str(text or "").strip()
# Phase 1: detect an explicit mode intent and switch first when present.
intent = (
handle_user_intent(raw_text, language=self._cfg.language)
if raw_text
else {
"matched": False,
"pure_switch": False,
"mode": "",
"remaining_text": "",
}
)
async with _plugin_lock(self._lock):
active_mode = self._state.active_mode
mode_switch: dict[str, Any] = {}
if intent.get("matched") and intent.get("kind") == "mode_switch":
try:
mode_switch = await self._apply_mode_switch(
str(intent.get("mode") or MODE_COMPANION),
f"intent:{intent.get('keyword') or 'text'}",
language=self._cfg.language,
)
active_mode = str(mode_switch.get("new_mode") or active_mode)
except ValueError as exc:
return _entry_exception_error(self, exc, operation="study_explain_text")
if intent.get("pure_switch"):
transition_phrase = str(
mode_switch.get("transition_phrase")
or intent.get("transition_phrase")
or ""
)
return Ok(
{
**mode_switch,
"reply": transition_phrase,
"summary": transition_phrase,
"operation": MODE_CONCEPT_EXPLAIN,
"input_text": raw_text,
"degraded": False,
}
)
# Phase 2: resolve the text to explain.
intent_kind = str(intent.get("kind") or "")
source_text = str(intent.get("remaining_text") or "").strip()
if not source_text and intent_kind != "concept_explain":
source_text = raw_text
vision_image_payload = str(vision_image_base64 or "").strip()
used_ocr_fallback = False
if not source_text and not vision_image_payload:
async with _plugin_lock(self._lock):
source_text = self._state.last_ocr_text
used_ocr_fallback = bool(source_text.strip())
source_text = source_text.strip()
if not source_text and not vision_image_payload:
return Err(
SdkError(
"study tutor requires text or a non-empty OCR snapshot",
code="MISSING_TEXT",
)
)
# Phase 3: explain with the active mode selected above.
try:
image_only_source = False
if vision_image_payload:
validated_vision_image = _validate_optional_vision_image_payload(
self, vision_image_payload, operation="study_explain_text"
)
if isinstance(validated_vision_image, Err):
return validated_vision_image
vision_image_payload = validated_vision_image
if not source_text:
source_text = _image_only_explain_prompt(self._cfg.language)
image_only_source = True
extra_context: dict[str, Any] = {
"source": "ocr_snapshot"
if used_ocr_fallback
else ("vision_image" if image_only_source else "manual"),
"mode": active_mode,
"mode_switch": bool(mode_switch.get("changed")),
"source_text": source_text,
}
if vision_image_payload:
extra_context["vision_enabled"] = True
extra_context["vision_image_base64"] = vision_image_payload
tutor_context = await self._build_learning_context(
LLM_OPERATION_CONCEPT_EXPLAIN,
input_text=source_text,
extra=extra_context,
)
reply = await self._agent.concept_explain(
source_text,
mode=active_mode,
context=tutor_context,
)
payload = await self._finalize_tutor_call(
LLM_OPERATION_CONCEPT_EXPLAIN,
reply,
history_kind=MODE_CONCEPT_EXPLAIN,
metadata={
"degraded": reply.degraded,
"diagnostic": reply.diagnostic,
"mode": active_mode,
"mode_switch": mode_switch,
"intent": intent,
"screen_classification": tutor_context.get("screen_classification")
or {},
},
extra_context=tutor_context,
)
if mode_switch:
payload["mode_switch"] = mode_switch
if intent.get("matched"):
payload["intent"] = intent
if intent.get("pure_switch"):
payload["transition_phrase"] = str(
mode_switch.get("transition_phrase")
or intent.get("transition_phrase")
or ""
)
return Ok(payload)
except Exception as exc:
return _entry_exception_error(self, exc, operation="study_explain_text")