generated from exteraGramPlugins/pluginBase
Implement Chat Summaries plugin
Replace the sample plugin with an AI chat summarizer that renders its
output as a native RichMessage injected into the chat.
Summarization pipeline:
- Fetch chat/topic history via paginated TL requests.
- Send a system prompt, the user's instructions and an anonymized JSON
transcript as three separate roles. Only message text and attachment
names leave the device; senders and message ids are replaced by
indices, and out-of-range reply parents are included with negative ids
so replies keep their context.
- Stream the response over SSE, reading reasoning from reasoning_content,
reasoning or inline <think> tags.
- Substitute the model's {userN}/{msgN} placeholders back into real names
and deep links. Placeholders are used instead of Markdown links because
a half-streamed link renders as broken literal text.
Rendering:
- Compile Markdown into TL_iv page blocks (headings, lists, quotes, code,
dividers, inline styles) since the app's own converter works off
Android Spanned spans rather than Markdown source.
- Show progress stages immediately on click, animate reasoning while it
streams, then collapse it into a tappable details block.
- Attribute the card to a client-side mock sender and insert it directly
after the newest analyzed message, scrolling it into view.
Persistence:
- Store summaries in SQLite keyed by (dialog, topic, anchor) and
re-inject them when the chat is reopened.
Also add tools/release.py and a Gitea workflow that builds, verifies and
publishes dist/<id>.plugin to the Releases tab, and extend
tools/verify_bundle.py to 104 offline checks covering the transcript,
streaming, rendering, injection and storage behaviour.
This commit is contained in:
1 parent
a3b0b264c1
commit
196da742de
27 files changed
+5261
-121
No files matched your search
@@ -0,0 +1,129 @@
|
||||
"""Bottom-line UI: prompts for message count/offset/prompt, runs the
|
||||
summarization, and shows the result. Deliberately plain — no bottom sheets,
|
||||
sliders, or styling copied from any other plugin.
|
||||
"""
|
||||
from typing import Any, Callable
|
||||
|
||||
from android.widget import LinearLayout, TextView
|
||||
from org.telegram.messenger import AndroidUtilities
|
||||
from org.telegram.ui.Components import EditTextBoldCursor
|
||||
|
||||
import diagnostics
|
||||
from ui.alert import AlertDialogBuilder
|
||||
from ui.bulletin import BulletinHelper
|
||||
from ui.theme import DialogColors
|
||||
from config import (
|
||||
DEFAULT_MSG_COUNT,
|
||||
DEFAULT_MSG_OFFSET,
|
||||
MAX_MSG_COUNT,
|
||||
MIN_MSG_COUNT,
|
||||
)
|
||||
|
||||
|
||||
def _dp(value: int) -> int:
|
||||
return AndroidUtilities.dp(value)
|
||||
|
||||
|
||||
def _match_wrap_params() -> Any:
|
||||
"""LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).
|
||||
|
||||
Uses plain ints (-1 / -2, the real values behind
|
||||
ViewGroup.LayoutParams.MATCH_PARENT/WRAP_CONTENT) instead of
|
||||
org.telegram.ui.Components.LayoutHelper: on this device/SDK build,
|
||||
Chaquopy raises AttributeError resolving LayoutHelper.MATCH_PARENT even
|
||||
though the Java field exists, while WRAP_CONTENT alone was not enough
|
||||
to prove the whole class is reachable. Plain LinearLayout.LayoutParams
|
||||
sidesteps that static-field lookup entirely.
|
||||
"""
|
||||
return LinearLayout.LayoutParams(-1, -2)
|
||||
|
||||
|
||||
def _make_label(activity, text: str, colors: DialogColors) -> TextView:
|
||||
label = TextView(activity)
|
||||
label.setText(text)
|
||||
label.setTextSize(14)
|
||||
label.setPadding(0, _dp(12), 0, _dp(4))
|
||||
label.setTextColor(colors.secondary_text)
|
||||
return label
|
||||
|
||||
|
||||
def _make_edit_text(activity, hint: str, initial: str, colors: DialogColors) -> Any:
|
||||
field = EditTextBoldCursor(activity)
|
||||
field.setHint(hint)
|
||||
field.setText(initial)
|
||||
field.setSingleLine(True)
|
||||
field.setTextColor(colors.input_text)
|
||||
field.setHintTextColor(colors.hint_text)
|
||||
field.setLineColors(colors.input_line, colors.input_line_active, colors.input_line_active)
|
||||
field.setCursorColor(colors.cursor)
|
||||
return field
|
||||
|
||||
|
||||
def show_summarize_dialog(
|
||||
activity: Any,
|
||||
fragment: Any,
|
||||
default_count: int,
|
||||
default_offset: int,
|
||||
default_prompt: str,
|
||||
on_submit: Callable[[int, int, str], None],
|
||||
) -> None:
|
||||
"""Shows a dialog collecting message count, offset, and prompt, then
|
||||
invokes on_submit(count, offset, prompt) if the user confirms.
|
||||
"""
|
||||
colors = DialogColors()
|
||||
root = LinearLayout(activity)
|
||||
root.setOrientation(LinearLayout.VERTICAL)
|
||||
root.setPadding(_dp(24), _dp(8), _dp(24), _dp(8))
|
||||
root.setBackgroundColor(colors.background)
|
||||
|
||||
count_label = _make_label(activity, "Messages to summarize (1-1000)", colors)
|
||||
root.addView(count_label)
|
||||
count_field = _make_edit_text(
|
||||
activity, "e.g. 100", str(max(MIN_MSG_COUNT, min(MAX_MSG_COUNT, default_count))), colors
|
||||
)
|
||||
root.addView(count_field, _match_wrap_params())
|
||||
|
||||
offset_label = _make_label(activity, "Offset from newest message", colors)
|
||||
root.addView(offset_label)
|
||||
offset_field = _make_edit_text(activity, "e.g. 0", str(max(0, default_offset)), colors)
|
||||
root.addView(offset_field, _match_wrap_params())
|
||||
|
||||
prompt_label = _make_label(activity, "Prompt (saved for this chat/topic)", colors)
|
||||
root.addView(prompt_label)
|
||||
prompt_field = _make_edit_text(activity, "Summarize this conversation…", default_prompt, colors)
|
||||
prompt_field.setSingleLine(False)
|
||||
prompt_field.setMinLines(2)
|
||||
prompt_field.setMaxLines(5)
|
||||
root.addView(prompt_field, _match_wrap_params())
|
||||
|
||||
builder = AlertDialogBuilder(activity)
|
||||
builder.set_title("Summarize Chat")
|
||||
builder.set_view(root)
|
||||
|
||||
def on_positive(bld, which):
|
||||
try:
|
||||
count = int(str(count_field.getText()).strip() or DEFAULT_MSG_COUNT)
|
||||
except ValueError:
|
||||
count = DEFAULT_MSG_COUNT
|
||||
try:
|
||||
offset = int(str(offset_field.getText()).strip() or DEFAULT_MSG_OFFSET)
|
||||
except ValueError:
|
||||
offset = DEFAULT_MSG_OFFSET
|
||||
count = max(MIN_MSG_COUNT, min(MAX_MSG_COUNT, count))
|
||||
offset = max(0, offset)
|
||||
prompt = str(prompt_field.getText()).strip()
|
||||
bld.dismiss()
|
||||
try:
|
||||
on_submit(count, offset, prompt)
|
||||
except Exception as exc:
|
||||
diagnostics.record_error("summarize_dialog.on_submit", exc)
|
||||
BulletinHelper.show_error(f"Summarize failed to start: {exc}", fragment)
|
||||
|
||||
def on_negative(bld, which):
|
||||
bld.dismiss()
|
||||
|
||||
builder.set_positive_button("Summarize", on_positive)
|
||||
builder.set_negative_button("Cancel", on_negative)
|
||||
builder.show()
|
||||
|
||||
|
||||
Reference in new issue
Block a user