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:
g1nation
2026-05-25 09:59:32 +09:00
parent 4153f640c2
commit 0a97324f1b
149 changed files with 14628 additions and 6927 deletions
+56 -16
View File
@@ -110,6 +110,22 @@ export interface SectionOutline {
scope: string;
}
/**
* ChunkedWriter 의 4개 persona 를 *외부에서 주입* 가능하게 하는 컨테이너.
*
* 의도: 사용자가 ChunkedWriter 를 그대로 쓰면서 일부 role 의 톤만 바꾸고 싶을 때
* (예: 자기 회사 도메인 어휘로 polish 톤 커스텀, 또는 영어 답변 모드 outline)
* 새 클래스 wrap 없이 한 줄로 처리 가능하게.
*
* 각 필드 미지정 시 기본 persona (`DEFAULT_*_PERSONA`) 사용.
*/
export interface PersonaOverrides {
outline?: string;
section?: string;
polish?: string;
direct?: string;
}
/**
* ChunkedWriter — single-agent replacement for the old 5-stage pipeline.
*
@@ -137,15 +153,9 @@ export interface SectionOutline {
* abstraction loss. The only thing that changes is the per-call system
* prompt picked here based on `options.config.role`.
*/
export class ChunkedWriter extends BaseAgent {
/**
* Hard ceiling — *사용자 config 가 어떤 값이든 이걸 넘을 수 없다*. 안전망 의미.
* 실제 사용 상한은 `getConfig().chunkedMaxSections` (default 3). 사용자가
* Astra Settings 에서 1~10 사이 조정 가능, 이 상수가 그 위 절대 한도.
*/
static readonly MAX_SECTIONS_HARD_CEILING = 10;
// ─── Default personas (module-level exports — 외부에서 import / 부분 override 가능) ───
private readonly outlinePersona = `You are a concise editor planning the structure of a Korean answer.
export const DEFAULT_OUTLINE_PERSONA = `You are a concise editor planning the structure of a Korean answer.
Decide how many sections the answer needs. The exact upper bound (MAX_N) is given in the user message below — never exceed it. Pick the *smallest* count that still covers the request well — a short factual question should be 0-1 section, a meaty analysis up to MAX_N.
Output STRICTLY a JSON array of objects: \`[{"heading": "...", "scope": "..."}]\`. No prose, no fences, no leading text.
@@ -159,7 +169,7 @@ Output STRICTLY a JSON array of objects: \`[{"heading": "...", "scope": "..."}]\
If the user attached source content (article/code/log) the sections must cover *that content*, not analysis methodology.`;
private readonly sectionPersona = `You are writing ONE section of a longer Korean answer. You will be given:
export const DEFAULT_SECTION_PERSONA = `You are writing ONE section of a longer Korean answer. You will be given:
- the user's original request (possibly with attached content),
- this section's heading + scope,
- the full outline (for context only — DO NOT write other sections),
@@ -173,7 +183,7 @@ Rules:
- If the user attached source content, cite from it; do not invent facts.
- Do NOT output the heading itself — only the body of this section.`;
private readonly polishPersona = `You are the final editor producing the user-facing Korean answer from a sectioned draft.
export const DEFAULT_POLISH_PERSONA = `You are the final editor producing the user-facing Korean answer from a sectioned draft.
[Job]
1. Fix typos, broken markdown, inconsistent terminology.
@@ -215,12 +225,12 @@ B. **짧은 직답 (1~3문장 정도로 충분한 경우)**:
- 추론 과정·\`<think>\`·"Thinking Process:" 같은 hidden reasoning 절대 노출 금지.
- 본문 분기를 LLM 자신이 판단 — 사용자가 모드 명시 안 함.`;
/**
* Single-pass 직답 persona. 짧은 질문·정의 묻기·간단한 사실 확인처럼
* 쪼갤 필요 없는 입력을 1회 호출로 끝낸다. outline → section → polish 의
* 3회 LLM 호출을 통째로 우회 → 작은 모델로 즉답 가능.
*/
private readonly directPersona = `You are answering a Korean user request in one shot. No outline, no drafting — just the final answer.
/**
* Single-pass 직답 persona. 짧은 질문·정의 묻기·간단한 사실 확인처럼
* 쪼갤 필요 없는 입력을 1회 호출로 끝낸다. outline → section → polish 의
* 3회 LLM 호출을 통째로 우회 → 작은 모델로 즉답 가능.
*/
export const DEFAULT_DIRECT_PERSONA = `You are answering a Korean user request in one shot. No outline, no drafting — just the final answer.
Rules:
- 첫 문장이 결론 / 직답이다. "분석해보겠습니다" "좋은 질문입니다" 같은 서문 금지.
@@ -231,6 +241,36 @@ Rules:
- 사용자가 본문(코드·기사·로그)을 첨부했으면 그 본문에서 인용. 본문에 없는 사실 지어내지 말 것.
- 추론 과정·"Thinking:"·<think> 노출 금지.`;
export class ChunkedWriter extends BaseAgent {
/**
* Hard ceiling — *사용자 config 가 어떤 값이든 이걸 넘을 수 없다*. 안전망 의미.
* 실제 사용 상한은 `getConfig().chunkedMaxSections` (default 3). 사용자가
* Astra Settings 에서 1~10 사이 조정 가능, 이 상수가 그 위 절대 한도.
*/
static readonly MAX_SECTIONS_HARD_CEILING = 10;
/**
* 활성 persona. 기본은 DEFAULT_*_PERSONA, constructor 의 overrides 로 부분 교체.
* private 가 아닌 protected 로 둬서 subclass 가 진화시킬 수 있게.
*/
protected readonly outlinePersona: string;
protected readonly sectionPersona: string;
protected readonly polishPersona: string;
protected readonly directPersona: string;
/**
* @param modelName Ollama / LM Studio 모델 식별자
* @param overrides 4개 persona 중 일부만 교체 가능 (미지정 필드는 default 유지).
* 외부 plugin / 도메인 특화 사용처에서 톤 조정 시 사용.
*/
constructor(modelName: string, overrides?: PersonaOverrides) {
super(modelName);
this.outlinePersona = overrides?.outline ?? DEFAULT_OUTLINE_PERSONA;
this.sectionPersona = overrides?.section ?? DEFAULT_SECTION_PERSONA;
this.polishPersona = overrides?.polish ?? DEFAULT_POLISH_PERSONA;
this.directPersona = overrides?.direct ?? DEFAULT_DIRECT_PERSONA;
}
async execute(input: string, context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string> {
const role = (options?.config?.role as string | undefined) || 'section';
switch (role) {