feat: AI 채팅 다듬기 — 저장소 컨텍스트·표시제목·마크다운 렌더·말투
- 컨텍스트에 저장소 목록 추가(RepoService.findAll), slug 빼고 표시 제목만 노출 - AI 응답 마크다운 렌더링: shared/utils/markdown.ts (escapeHtml 후 신뢰 태그만 조립, 의존성 0, XSS 안전 — 굵게/목록/코드/제목/링크) - 시스템 프롬프트: 간결·이모지/사과 남발 금지·과도한 존칭 지양·마크다운 사용·저장소는 표시제목으로 안내 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TaskModule } from '../task/task.module';
|
||||
import { RepoModule } from '../repo/repo.module';
|
||||
import { AiController } from './ai.controller';
|
||||
import { AiService } from './ai.service';
|
||||
import { AiConversation } from './entities/ai-conversation.entity';
|
||||
import { AiMessage } from './entities/ai-message.entity';
|
||||
|
||||
// AI 모듈 — Claude(Anthropic) 채팅 프록시 + 대화/메시지 영속.
|
||||
// TaskModule 을 가져와 사용자 업무 컨텍스트를 시스템 프롬프트에 주입한다.
|
||||
// Task/RepoModule 을 가져와 사용자 업무·저장소 컨텍스트를 시스템 프롬프트에 주입한다.
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AiConversation, AiMessage]), TaskModule],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([AiConversation, AiMessage]),
|
||||
TaskModule,
|
||||
RepoModule,
|
||||
],
|
||||
controllers: [AiController],
|
||||
providers: [AiService],
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../../common/pagination/pagination';
|
||||
import { TaskService } from '../task/task.service';
|
||||
import type { TaskStatus } from '../task/entities/task.entity';
|
||||
import { RepoService } from '../repo/repo.service';
|
||||
import { AiConversation } from './entities/ai-conversation.entity';
|
||||
import { AiMessage } from './entities/ai-message.entity';
|
||||
|
||||
@@ -41,6 +42,8 @@ const MAX_TOKENS = 1024;
|
||||
const TITLE_MAX = 60;
|
||||
// 컨텍스트로 주입할 담당 업무 최대 건수(토큰 절약)
|
||||
const CONTEXT_TASK_LIMIT = 30;
|
||||
// 컨텍스트로 주입할 저장소 최대 개수
|
||||
const CONTEXT_REPO_LIMIT = 30;
|
||||
|
||||
// 주제 한정(가드레일) + 데이터 근거 지시
|
||||
const SYSTEM_PROMPT = [
|
||||
@@ -51,9 +54,11 @@ const SYSTEM_PROMPT = [
|
||||
'- 업무와 무관한 주제(일반 상식, 시사, 번역, 코딩 과제, 잡담 등)는 정중히 거절하고 업무 관련 질문을 하도록 안내하세요.',
|
||||
'',
|
||||
'[규칙]',
|
||||
'- 한국어로 간결하고 정확하게 답하세요.',
|
||||
'- 한국어로 간결하고 명확하게 답하세요. 불필요한 인사말·사과·이모지 남발을 피하고, 과도한 존칭("회원님" 등) 대신 자연스러운 존댓말을 쓰세요.',
|
||||
'- 목록이나 강조가 필요하면 마크다운(굵게 **텍스트**, 목록 - 또는 1.)을 사용하세요. 표·복잡한 서식은 피하세요.',
|
||||
"- 아래 '사용자 데이터' 에 근거해 답하고, 데이터에 없는 내용은 추측하지 말고 모른다고 하세요.",
|
||||
"- 마감/일정은 제공된 '오늘 날짜' 를 기준으로 계산하세요.",
|
||||
'- 저장소는 영문 내부 이름이 아니라 한글 표시 제목으로 설명하세요.',
|
||||
].join('\n');
|
||||
|
||||
// 업무 상태 → 한국어 라벨(컨텍스트 표기용)
|
||||
@@ -76,6 +81,7 @@ export class AiService {
|
||||
constructor(
|
||||
config: ConfigService,
|
||||
private readonly taskService: TaskService,
|
||||
private readonly repoService: RepoService,
|
||||
@InjectRepository(AiConversation)
|
||||
private readonly convRepo: Repository<AiConversation>,
|
||||
@InjectRepository(AiMessage)
|
||||
@@ -264,11 +270,17 @@ export class AiService {
|
||||
.toISOString()
|
||||
.slice(0, 10); // KST 기준 오늘
|
||||
try {
|
||||
const [assigned, issued] = await Promise.all([
|
||||
this.taskService.listAssigned(userId, 1, CONTEXT_TASK_LIMIT),
|
||||
this.taskService.listIssued(userId, 1, 1),
|
||||
]);
|
||||
const assigned = await this.taskService.listAssigned(
|
||||
userId,
|
||||
1,
|
||||
CONTEXT_TASK_LIMIT,
|
||||
);
|
||||
const issued = await this.taskService.listIssued(userId, 1, 1);
|
||||
const repos = await this.repoService.findAll(1, CONTEXT_REPO_LIMIT);
|
||||
|
||||
const lines = [`오늘 날짜: ${today} (KST)`, ''];
|
||||
|
||||
// 담당 업무
|
||||
lines.push(`내가 담당한 업무 (${assigned.total}건):`);
|
||||
if (assigned.items.length === 0) {
|
||||
lines.push('- (없음)');
|
||||
@@ -285,6 +297,23 @@ export class AiService {
|
||||
}
|
||||
}
|
||||
lines.push('', `내가 지시한 업무: ${issued.total}건`);
|
||||
|
||||
// 저장소 목록(조직 전체 — 인증 사용자는 모두 열람 가능)
|
||||
lines.push('', `저장소 목록 (${repos.total}개):`);
|
||||
if (repos.items.length === 0) {
|
||||
lines.push('- (없음)');
|
||||
} else {
|
||||
for (const r of repos.items) {
|
||||
const vis = r.visibility === 'private' ? '비공개' : '공개';
|
||||
// 표시 제목(name) 위주로 안내 — slug(영문 내부 이름)는 사용자 노출용이 아니라 제외
|
||||
lines.push(
|
||||
`- ${r.name} — ${vis}, 업무 ${r.doneCount}/${r.totalCount}건`,
|
||||
);
|
||||
}
|
||||
if (repos.total > repos.items.length) {
|
||||
lines.push(`- … 외 ${repos.total - repos.items.length}개`);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
} catch (e) {
|
||||
this.logger.warn(
|
||||
|
||||
Reference in New Issue
Block a user