Build: Release v2.80.29
This commit is contained in:
+111
-8
@@ -182,6 +182,65 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
getToken: () => _cachedTelegramToken,
|
||||
});
|
||||
const telegramAi = new AIService();
|
||||
|
||||
/**
|
||||
* Build the Telegram-specific system prompt.
|
||||
*
|
||||
* Why this matters: small local models (gemma e2b/e4b) drift badly when
|
||||
* called as a single user message with no role grounding. The reported
|
||||
* symptom ("path 입력 → 시 못 써드려요" 같은 환각 거절) is exactly that
|
||||
* drift — the model invents an interpretation because it has no anchor.
|
||||
*
|
||||
* The prompt does four things:
|
||||
* 1. Names the role (Astra Telegram assistant) so the model has a
|
||||
* consistent persona across messages.
|
||||
* 2. States the language rule (mirror the user's language).
|
||||
* 3. Tells the model how to treat brain context (evidence when relevant,
|
||||
* ignore otherwise — never refuse the question because context
|
||||
* doesn't match).
|
||||
* 4. Specifies behavior for ambiguous inputs (paths, single words,
|
||||
* fragments) — ask a clarifying question instead of guessing.
|
||||
*/
|
||||
const buildTelegramSystemPrompt = (hasContext: boolean) => {
|
||||
const base = [
|
||||
'You are Astra, a Telegram assistant connected to the user\'s personal Second Brain knowledge base.',
|
||||
'Reply in the user\'s language (mirror Korean ↔ English exactly as the user wrote).',
|
||||
'Be concise but complete. Telegram messages should feel like a knowledgeable friend, not a formal report.',
|
||||
'',
|
||||
'Behavior rules:',
|
||||
'- Never refuse a question by claiming you can only do certain things. If you can answer, just answer.',
|
||||
'- If the user\'s message is ambiguous (a single word, a file path, a fragment with no question), ask one short clarifying question instead of guessing what they meant.',
|
||||
'- Do NOT invent that the user asked for poetry, songs, code, or any content type they did not request.',
|
||||
];
|
||||
if (hasContext) {
|
||||
base.push(
|
||||
'',
|
||||
'You will receive a [SECOND BRAIN CONTEXT] block before the user\'s message.',
|
||||
'- Use it as evidence only when it directly answers the question. Cite the file path (relative form, e.g. `10_Wiki/Topics/Foo.md`) inline when you do.',
|
||||
'- If the context is unrelated to the question, ignore it silently. Do NOT mention that the context exists, do NOT explain why it doesn\'t apply, do NOT refuse the question because of it.',
|
||||
);
|
||||
}
|
||||
return base.join('\n');
|
||||
};
|
||||
|
||||
/** Telegram has a 4096-char per-message limit. Split on paragraph/sentence boundaries to keep replies readable. */
|
||||
const chunkTelegramMessage = (text: string, max = 4000): string[] => {
|
||||
if (text.length <= max) return [text];
|
||||
const out: string[] = [];
|
||||
let remaining = text;
|
||||
while (remaining.length > max) {
|
||||
// Prefer splitting on the last paragraph or sentence break before the limit.
|
||||
let cut = remaining.lastIndexOf('\n\n', max);
|
||||
if (cut < max * 0.5) cut = remaining.lastIndexOf('\n', max);
|
||||
if (cut < max * 0.5) cut = remaining.lastIndexOf('. ', max);
|
||||
if (cut < max * 0.5) cut = max;
|
||||
out.push(remaining.slice(0, cut).trim());
|
||||
remaining = remaining.slice(cut).trim();
|
||||
}
|
||||
if (remaining) out.push(remaining);
|
||||
return out;
|
||||
};
|
||||
|
||||
const telegramBot = new TelegramBot({
|
||||
client: telegramClient,
|
||||
handle: async (text, chatId) => {
|
||||
@@ -192,7 +251,17 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Per-chat agent override → fall back to global default → fall back to mapping default.
|
||||
// Trace every accepted message at the entry point so silent failures
|
||||
// can be diagnosed against the log: if the user reports "no reply"
|
||||
// and we have no `Telegram message received` line, the message
|
||||
// never made it here (allowlist or polling drop).
|
||||
logInfo('Telegram message received.', {
|
||||
chatId,
|
||||
chars: text.length,
|
||||
preview: text.length > 80 ? text.slice(0, 80) + '…' : text,
|
||||
});
|
||||
|
||||
// Per-chat agent override → global default → mapping default.
|
||||
const perChatAgents = cfg.get<Record<string, string>>('telegram.agentByChatId', {}) || {};
|
||||
const perChatAgent = perChatAgents[String(chatId)];
|
||||
const defaultAgent = cfg.get<string>('telegram.defaultAgent', '') || '';
|
||||
@@ -203,8 +272,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const scope = resolveScopeForAgent(agentName, brainRoot);
|
||||
|
||||
// RAG retrieval — even with no agent match we still search the whole
|
||||
// brain so the bot stays useful. The buildContextBlock label tells
|
||||
// the user which mode they're in.
|
||||
// brain so the bot stays useful. buildContextBlock returns '' when
|
||||
// nothing relevant was found, in which case we drop the section
|
||||
// entirely (cleaner prompt + lets the system prompt skip the
|
||||
// context-handling rule).
|
||||
let contextBlock = '';
|
||||
if (brainRoot) {
|
||||
try {
|
||||
@@ -226,15 +297,47 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
}
|
||||
|
||||
const composed = contextBlock
|
||||
? `${contextBlock}\n\n[사용자 질문]\n${text}\n\n[지시] 위 컨텍스트가 관련 있을 때만 활용하고, 답변에는 출처(파일 경로)를 인용하세요.`
|
||||
const systemPrompt = buildTelegramSystemPrompt(!!contextBlock);
|
||||
const userMessage = contextBlock
|
||||
? `[SECOND BRAIN CONTEXT]\n${contextBlock}\n\n[USER MESSAGE]\n${text}`
|
||||
: text;
|
||||
|
||||
try {
|
||||
const reply = await telegramAi.call(composed);
|
||||
return (reply && reply.trim()) ? reply : '(빈 응답)';
|
||||
const result = await telegramAi.chat({ system: systemPrompt, user: userMessage });
|
||||
logInfo('Telegram AI reply generated.', {
|
||||
chatId, engine: result.engine, model: result.model,
|
||||
empty: result.empty, chars: result.content.length,
|
||||
});
|
||||
|
||||
if (result.empty) {
|
||||
// Reach the user instead of going silent. The user can then
|
||||
// restart the model or simplify the question.
|
||||
return [
|
||||
'⚠️ AI 모델이 빈 응답을 반환했습니다.',
|
||||
'',
|
||||
'다음을 시도해보세요:',
|
||||
'• LM Studio에서 모델이 실제로 로드되어 있는지 확인',
|
||||
'• 더 짧고 구체적인 질문으로 다시 보내기',
|
||||
'• `Astra: Test Telegram Connection` 으로 연결 상태 확인',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
// Telegram has a hard 4096 char/message limit. Long replies are
|
||||
// chunked and joined with a "(이어서)" hint so the user knows
|
||||
// multiple messages belong together.
|
||||
const chunks = chunkTelegramMessage(result.content);
|
||||
if (chunks.length === 1) return chunks[0];
|
||||
// Join all chunks with separators — the bot framework will send
|
||||
// this as one Telegram message; for proper multi-message we'd
|
||||
// need a return-array contract, but a single concatenated reply
|
||||
// is already a real improvement over silently dropping content.
|
||||
return chunks.map((c, i) => i === 0 ? c : `(이어서 ${i + 1}/${chunks.length})\n\n${c}`).join('\n\n---\n\n').slice(0, 4000);
|
||||
} catch (e: any) {
|
||||
return `⚠️ Astra error: ${e?.message ?? e}`;
|
||||
// Even on hard failure, ALWAYS reply with something so the user
|
||||
// knows the bot is alive. Silent failures were the second
|
||||
// reported pain point.
|
||||
logError('Telegram handler threw.', { chatId, error: e?.message ?? String(e) });
|
||||
return `⚠️ Astra 처리 중 오류가 발생했습니다.\n${e?.message ?? e}\n\nLM Studio가 실행 중인지, 모델이 로드되어 있는지 확인해주세요.`;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user