feat: v2.2.92 → v2.2.158 — god-file 분해 + Stocks feature + 대화 연속성
R56–R59: agent.ts 2731→1529줄 god-file 분해 (25 modules) · attrParsers + LLM 메서드 8개 (callNonStreaming, streamChatOnce 등) · executeActions 415줄 → 8 handler 그룹 (file/run/list/brain/calendar/sheets/tasks) · handlePrompt 1100줄 → 7 phase 모듈 (system prompt + budget + autoContinue 등) R50–R55: extension.ts 1145→349줄 (telegram/settings/provider commands 분리) Stocks feature 신규: /stocks slash command (v2.2.152~158) · .astra/stocks.json 저장소 + Yahoo Finance 현재가 갱신 · 8 키워드 필터 (ROE/성장성/유동성/수익성/영업효율/기술력/안정성/PBR) · Naver 시가총액 페이지 JSON API (m.stock.naver.com) 발굴 · LLM Top 5 매력도 분석 + Telegram 자동 보고서 · KST 09:00/15:00 watcher 자동 모니터링 대화 연속성 (v2.2.150~157): · [PRIOR TURN CONCLUSION] block 으로 직전 결론 anchor · thin follow-up 분류 → boilerplate 헤더 suppression · slash 명령 결과 chatHistory mirror (capture wrapper) · echo/parrot 금지 system prompt rule 기타: /stocks 슬래시 자동완성 dropdown UI, Naver JSON API 전환 (cheerio 제거) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { TelegramBot } from './telegramBot';
|
||||
import type { TelegramHttpClient } from './telegramClient';
|
||||
import { AIService } from '../../core/services';
|
||||
import { getActiveBrainProfile, logInfo, logError } from '../../utils';
|
||||
import { resolveScopeForAgent } from '../../skills/agentKnowledgeMap';
|
||||
import { retrieveScoped, buildContextBlock } from '../../skills/scopedBrainRetriever';
|
||||
import type { SidebarChatProvider } from '../../sidebarProvider';
|
||||
import {
|
||||
buildTelegramSystemPrompt,
|
||||
looksLikeWorkOrder,
|
||||
buildTelegramCompanyContext,
|
||||
chunkTelegramMessage,
|
||||
} from './promptBuilders';
|
||||
|
||||
export interface TelegramSetupDeps {
|
||||
telegramClient: TelegramHttpClient;
|
||||
/** activate() 안 `let provider` 의 *호출 시점* 값. 등록 시점엔 undefined 일 수 있음. */
|
||||
getProvider: () => SidebarChatProvider | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Telegram bot 생성 + 메시지 처리 콜백.
|
||||
*
|
||||
* 흐름:
|
||||
* 1) allowlist 검사 (`telegram.allowedChatIds`) — 통과 못 하면 silent drop
|
||||
* 2) 회사 모드 + work-order 라우팅 → CEO 디스패처 (즉시 ack 반환)
|
||||
* 3) Per-chat agent 결정 + scoped RAG 검색
|
||||
* 4) 시스템 프롬프트 (company-aware) + 대화 히스토리 + 현재 메시지 결합
|
||||
* 5) AI 호출 → empty/error 시 친절한 fallback 메시지
|
||||
* 6) reply 저장 + 4096자 청크 분할
|
||||
*
|
||||
* 모든 단계에서 "조용히 실패하지 말 것" 이 가장 중요 — 사용자가 "응답 없음" 을
|
||||
* 가장 큰 통증으로 보고했음. 빈 응답이나 예외에도 항상 사람이 읽을 수 있는
|
||||
* 메시지를 반환.
|
||||
*/
|
||||
export function createTelegramBot(
|
||||
context: vscode.ExtensionContext,
|
||||
deps: TelegramSetupDeps,
|
||||
): TelegramBot {
|
||||
const { telegramClient, getProvider } = deps;
|
||||
const telegramAi = new AIService();
|
||||
|
||||
return new TelegramBot({
|
||||
client: telegramClient,
|
||||
handle: async (text, chatId) => {
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||
const allowed = cfg.get<number[]>('telegram.allowedChatIds', []) || [];
|
||||
if (allowed.length > 0 && !allowed.includes(chatId)) {
|
||||
logInfo('Telegram message from unallowed chat ignored.', { chatId });
|
||||
return null;
|
||||
}
|
||||
|
||||
// 진입점 trace — silent failure 진단용. "응답 없음" 신고 시 이 로그가
|
||||
// 없으면 메시지가 여기까지도 못 옴 (allowlist 또는 polling drop).
|
||||
logInfo('Telegram message received.', {
|
||||
chatId,
|
||||
chars: text.length,
|
||||
preview: text.length > 80 ? text.slice(0, 80) + '…' : text,
|
||||
});
|
||||
|
||||
// ── 1인 기업 모드 라우팅 ────────────────────────────────────────
|
||||
// 회사 모드 ON + 메시지가 work *order* 처럼 보이면 (만들어줘/해줘 또는
|
||||
// "CEO한테 …" 접두) RAG-chat 대신 dispatcher 로. dispatcher 가 끝에
|
||||
// Telegram mirror 를 쏘므로 사용자는 제대로 된 보고서를 받음.
|
||||
const { readCompanyState } = await import('../../features/company');
|
||||
const companyState = readCompanyState(context);
|
||||
if (companyState.enabled && looksLikeWorkOrder(text)) {
|
||||
const { appendTelegramMessage } = await import('./conversationHistory');
|
||||
appendTelegramMessage({ chatId, role: 'user', text, kind: 'user' });
|
||||
logInfo('Telegram: routing to company turn.', { chatId, preview: text.slice(0, 60) });
|
||||
// Fire-and-forget — dispatcher 의 secretary mirror 가 최종 보고 전송.
|
||||
// 즉시 ack 를 반환해 사용자는 봇이 명령을 받았다는 신호를 봄.
|
||||
void (async () => {
|
||||
try {
|
||||
await getProvider()!._runCompanyTurn(text);
|
||||
} catch (e: any) {
|
||||
logError('Telegram → company turn failed.', { error: e?.message ?? String(e) });
|
||||
}
|
||||
})();
|
||||
const ack = '🧭 CEO에게 전달했어요. 작업 끝나면 보고드릴게요.';
|
||||
appendTelegramMessage({ chatId, role: 'assistant', text: ack, kind: 'reply' });
|
||||
return ack;
|
||||
}
|
||||
|
||||
// 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', '') || '';
|
||||
const agentName = (perChatAgent || defaultAgent || '').trim();
|
||||
|
||||
const brain = getActiveBrainProfile();
|
||||
const brainRoot = brain?.localBrainPath || '';
|
||||
const scope = resolveScopeForAgent(agentName, brainRoot);
|
||||
|
||||
// RAG retrieval — agent 매치 없어도 전체 brain 검색해 봇이 계속 유용하게.
|
||||
// buildContextBlock 가 '' 반환하면 섹션 자체를 빼버림 (cleaner prompt).
|
||||
let contextBlock = '';
|
||||
if (brainRoot) {
|
||||
try {
|
||||
const result = retrieveScoped(text, brainRoot, scope.folders, {
|
||||
maxResults: cfg.get<number>('telegram.contextChunks', 6) ?? 6,
|
||||
});
|
||||
contextBlock = buildContextBlock(result);
|
||||
logInfo('Telegram RAG retrieval done.', {
|
||||
chatId,
|
||||
agent: scope.agent?.name ?? '(none)',
|
||||
scopedFolders: scope.folders.length,
|
||||
candidates: result.candidateCount,
|
||||
chunks: result.chunks.length,
|
||||
});
|
||||
} catch (e: any) {
|
||||
logError('Telegram RAG retrieval failed; falling back to plain prompt.', {
|
||||
chatId, error: e?.message ?? String(e),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Company-aware 시스템 프롬프트 — 회사 모드 ON 일 때 봇은 그 회사의 *비서*.
|
||||
// 모델에게 그렇게 말해줘야 "어디에 저장했어?" 같은 질문에 "파일 시스템 접근
|
||||
// 못 함" 으로 회피하지 않고 실제 경로를 그대로 답함.
|
||||
const companyContextBlock = buildTelegramCompanyContext(companyState, context);
|
||||
const systemPrompt = buildTelegramSystemPrompt(!!contextBlock)
|
||||
+ (companyContextBlock ? `\n\n${companyContextBlock}` : '');
|
||||
|
||||
// Per-chat 대화 히스토리 — 없으면 매 inbound 가 fresh turn 이라
|
||||
// "방금 한 말" 을 봇이 즉시 잊음. AI service 의 {system, user} 표면이
|
||||
// messages 배열을 안 받아서 user 메시지에 inline 으로 박음.
|
||||
const { appendTelegramMessage, getRecentMessages, formatHistoryForPrompt } =
|
||||
await import('./conversationHistory');
|
||||
const history = getRecentMessages(chatId, 10);
|
||||
const historyBlock = formatHistoryForPrompt(history);
|
||||
const pieces: string[] = [];
|
||||
if (contextBlock) pieces.push(`[SECOND BRAIN CONTEXT]\n${contextBlock}`);
|
||||
if (historyBlock) pieces.push(historyBlock);
|
||||
pieces.push(`[USER MESSAGE]\n${text}`);
|
||||
const userMessage = pieces.join('\n\n');
|
||||
|
||||
// AI 호출 *전에* user 메시지 persist — 실패해도 다음 inbound 가 발화를 봄.
|
||||
appendTelegramMessage({ chatId, role: 'user', text, kind: 'user' });
|
||||
|
||||
try {
|
||||
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) {
|
||||
// silent 가 아니라 사용자에게 도달. 모델 재시작/질문 단순화 안내.
|
||||
return [
|
||||
'⚠️ AI 모델이 빈 응답을 반환했습니다.',
|
||||
'',
|
||||
'다음을 시도해보세요:',
|
||||
'• LM Studio에서 모델이 실제로 로드되어 있는지 확인',
|
||||
'• 더 짧고 구체적인 질문으로 다시 보내기',
|
||||
'• `Astra: Test Telegram Connection` 으로 연결 상태 확인',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
// assistant reply persist → 다음 inbound 가 우리가 한 말을 봄.
|
||||
appendTelegramMessage({ chatId, role: 'assistant', text: result.content, kind: 'reply' });
|
||||
// 4096자 hard limit 분할. 1청크면 그대로, 여러 청크는 "(이어서 i/n)"
|
||||
// 힌트와 함께 join — bot framework 가 한 메시지로 보내지만, drop 없이
|
||||
// 모든 청크를 시도하는 게 silent loss 보다 나음.
|
||||
const chunks = chunkTelegramMessage(result.content);
|
||||
if (chunks.length === 1) return chunks[0];
|
||||
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) {
|
||||
// 하드 실패에서도 ALWAYS 응답 — silent failure 가 두 번째로 보고된 통증.
|
||||
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