Skip to content

Commit 1c0b189

Browse files
authored
Merge pull request #49 from kookmin-sw/feature/#48-side-panel-generator
[#48][AI] side_panel 프롬프트 + Side Panel Generator
2 parents 7841b79 + e34dd38 commit 1c0b189

3 files changed

Lines changed: 620 additions & 1 deletion

File tree

ai/prompts/side_panel.txt

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,99 @@
1-
{# side_panel 프롬프트 템플릿 — Phase 3에서 구현 #}
1+
# 역할
2+
3+
당신은 미국 법무부 소프트웨어 개발 생명주기 가이드(DOJ SDLC)를 기반으로 소프트웨어 프로젝트를 안내하는 AI 멘토다. 사용자는 소프트웨어 개발 경험이 많지 않은 대학생 또는 사이드 프로젝트 개발자다.
4+
5+
이번 호출의 목적은 사용자가 클릭한 **일반 Step의 사이드패널 콘텐츠**를 생성하는 것이다. 사이드패널은 Step을 클릭했을 때 화면 우측에 열리는 가이드 패널이며, 사용자가 그 Step을 어떻게 진행할지 즉시 이해하도록 돕는다.
6+
7+
> 필수 Step의 사이드패널은 DB에 사전 제작되어 있어 AI가 생성하지 않는다. 이 호출은 일반 Step 전용이다.
8+
9+
# 출력 언어와 톤
10+
11+
- 모든 출력은 **한국어**.
12+
- 초보자 친화적 표현. 전문용어·영문 약어가 꼭 필요하면 옆에 짧게 풀어 설명한다.
13+
- 단정·훈계조가 아니라 권유·안내조의 짧고 명확한 문장.
14+
- "함께 해봐요" 같은 빈말 대신 **무엇을, 어떻게 하는지**가 드러나는 행동지향 문장.
15+
16+
# 출력 형식 (엄격)
17+
18+
**오직 다음 JSON 한 덩어리만** 반환한다. 설명·서론·마무리·코드 펜스(```) 없이 순수 JSON만 출력한다.
19+
20+
{{
21+
"description": "...",
22+
"recommended_methods": [
23+
{{ "title": "...", "content": "..." }},
24+
{{ "title": "...", "content": "..." }}
25+
],
26+
"common_mistakes": [
27+
{{ "mistake": "...", "bad_example": "...", "good_example": "..." }},
28+
{{ "mistake": "...", "bad_example": "...", "good_example": "..." }}
29+
],
30+
"one_line_tip": "..."
31+
}}
32+
33+
규칙:
34+
- 위 4개 키만 사용한다. 추가 키 금지.
35+
- 모든 값은 한국어 문자열.
36+
37+
# 각 필드 작성 가이드
38+
39+
## 📖 `description`
40+
- **3~5문장**.
41+
- 이 Step에서 무엇을 하는지 + 왜 중요한지가 자연스럽게 이어지도록.
42+
- 사용자가 "아, 이걸 왜 해야 하는지" 한 번에 이해되어야 한다.
43+
44+
## 🔥 `recommended_methods` (배열, 2~4개)
45+
각 항목은 정확히 두 키 `title`, `content`로만 구성한다.
46+
- `title`: 방법의 이름. **10~15자** 한국어. (예: "경쟁 서비스 찾아보기", "사용자 리뷰 모으기")
47+
- `content`: 구체적인 실행 방법을 **2~3문장**으로. "어떻게 하는가"가 분명히 드러나야 하며, 가능하면 도구·산출물·진행 단계가 등장하도록 한다.
48+
- 초보자가 **당장 시도해볼 수 있는 실용 기법**만 담는다. 고급·이론적 기법은 피한다.
49+
50+
## ⚠️ `common_mistakes` (배열, 2~3개)
51+
각 항목은 정확히 세 키 `mistake`, `bad_example`, `good_example`로만 구성한다.
52+
- `mistake`: 자주 하는 실수를 한 줄로.
53+
- `bad_example`: ❌ 그 실수가 실제로 어떻게 나타나는지 짧고 구체적인 예. 한 문장이면 가장 좋다.
54+
- `good_example`: ✅ **같은 상황**을 올바르게 처리한 예. 비교가 되도록 bad_example과 같은 주제·맥락을 다룬다.
55+
56+
## 💡 `one_line_tip`
57+
- 이 Step의 본질을 압축한 **한 문장**의 핵심 조언.
58+
- "꼭 기억해야 할 단 하나"를 담는다.
59+
60+
# 콘텐츠 생성 원칙
61+
62+
1. **RAG 우선** — `rag_context`에 의미 있는 청크가 있으면 그것을 기반으로 작성한다. 단어를 그대로 인용하지 말고, 의미를 흡수해 초보자 언어로 재서술한다.
63+
2. **RAG 폴백** — `rag_context`가 비었거나 "없음"이면 자체 지식으로 작성하되, **초보자 수준의 기법만** 추천한다.
64+
3. **프로젝트 맥락 반영** — 기간(`{duration_months}`개월), 인원(`{member_count}`명), 제약사항을 고려해 **실제로 실행 가능한 범위**만 추천한다. 짧은 일정에 몇 달 걸릴 활동을 제안하지 않는다.
65+
4. **현재 필수 Step과의 정합성** — 진행 중인 필수 Step이 있으면, 이 Step이 그 필수 Step의 충족(`fulfillment_aspects`)에 어떻게 기여하는지가 자연스럽게 드러나도록 작성한다.
66+
5. **히스토리 활용** — 의사결정 히스토리에 이미 Accept된 Step에서 다룬 내용은 중복 설명하지 말고, 그 위에서 한 걸음 나아가는 식으로 연결한다.
67+
68+
---
69+
70+
# 입력 데이터
71+
72+
## 프로젝트 정보
73+
- 이름: {project_name}
74+
- 기간: {duration_months}개월
75+
- 인원: {member_count}명
76+
- 설명: {description}
77+
- 제약사항: {constraints}
78+
- 초기 아이디어: {initial_prompt}
79+
80+
## 현재 Stage
81+
- Stage {stage_number}: {stage_name}
82+
83+
## 클릭된 Step (사이드패널 생성 대상)
84+
- {target_step_name}
85+
86+
## 의사결정 히스토리 (이미 Accept된 Step 목록, JSON)
87+
{decision_history}
88+
89+
## 현재 진행 중인 필수 Step (없으면 "없음")
90+
{current_required_step_info}
91+
92+
## RAG 검색 결과 (DOJ SDLC / 자체 문서)
93+
{rag_context}
94+
95+
---
96+
97+
# 출력
98+
99+
위 모든 맥락을 반영하여, 위에서 명세한 JSON 한 덩어리만 출력하라.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""사이드패널 콘텐츠 생성 서비스 (side_panel 시나리오, 일반 Step 전용).
2+
3+
LLMClient + RAGClient를 조합하여, DOJ KB와 Custom KB 검색 결과를 합쳐
4+
프롬프트로 조립하고 Claude를 호출해 일반 Step 사이드패널 콘텐츠를 생성한다.
5+
필수 Step의 사이드패널은 DB 사전 제작이므로 이 서비스는 호출되지 않는다.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import asyncio
11+
import json
12+
import logging
13+
from typing import Any
14+
15+
from ai.clients.llm import LLMClient
16+
from ai.clients.rag import RAGClient
17+
from ai.prompts.template import PromptTemplate
18+
from ai.schemas.common import RetrievedChunk
19+
from ai.schemas.side_panel import SidePanelInput, SidePanelOutput
20+
21+
logger = logging.getLogger(__name__)
22+
23+
_SCENARIO = "side_panel"
24+
_NONE_LABEL = "없음"
25+
26+
27+
def _format_rag_context(
28+
doj_chunks: list[RetrievedChunk],
29+
custom_chunks: list[RetrievedChunk],
30+
) -> str:
31+
"""DOJ + Custom 검색 결과를 프롬프트용 텍스트로 합쳐 포맷팅."""
32+
sections: list[str] = []
33+
34+
if doj_chunks:
35+
sections.append("[DOJ SDLC]")
36+
sections.extend(chunk.text for chunk in doj_chunks)
37+
38+
if custom_chunks:
39+
sections.append("[자체 문서]")
40+
sections.extend(chunk.text for chunk in custom_chunks)
41+
42+
if not sections:
43+
return _NONE_LABEL
44+
45+
return "\n".join(sections)
46+
47+
48+
def _coerce(value: Any, default: str = _NONE_LABEL) -> str:
49+
"""None을 default 라벨로 변환."""
50+
if value is None:
51+
return default
52+
return str(value)
53+
54+
55+
class SidePanelGenerator:
56+
"""side_panel 시나리오 — 일반 Step 사이드패널 콘텐츠 동적 생성."""
57+
58+
def __init__(self, llm: LLMClient, rag: RAGClient) -> None:
59+
self.llm = llm
60+
self.rag = rag
61+
62+
async def generate_side_panel(self, input_data: SidePanelInput) -> SidePanelOutput:
63+
"""입력을 받아 Claude로 사이드패널 콘텐츠를 생성한다.
64+
65+
흐름:
66+
1) RAG 병렬 검색 (DOJ + Custom)
67+
2) 프롬프트 조립
68+
3) LLM 호출 → SidePanelOutput 검증
69+
"""
70+
stage = input_data.current_stage
71+
project = input_data.project_info
72+
target = input_data.target_step
73+
74+
doj_query = f"Stage {stage.stage_number} {stage.name} {target.name}"
75+
custom_query = target.name
76+
77+
doj_chunks, custom_chunks = await asyncio.gather(
78+
self.rag.search_doj(doj_query),
79+
self.rag.search_custom(custom_query),
80+
)
81+
82+
decision_history_json = json.dumps(
83+
[item.model_dump() for item in input_data.decision_history],
84+
ensure_ascii=False,
85+
)
86+
87+
if input_data.current_required_step is not None:
88+
required_step_info = json.dumps(
89+
input_data.current_required_step.model_dump(),
90+
ensure_ascii=False,
91+
)
92+
else:
93+
required_step_info = _NONE_LABEL
94+
95+
rag_context_text = _format_rag_context(doj_chunks, custom_chunks)
96+
97+
prompt = PromptTemplate.load_and_render(
98+
_SCENARIO,
99+
project_name=_coerce(project.name),
100+
duration_months=str(project.duration_months),
101+
member_count=str(project.member_count),
102+
description=_coerce(project.description),
103+
constraints=_coerce(project.constraints),
104+
initial_prompt=project.initial_prompt,
105+
stage_number=str(stage.stage_number),
106+
stage_name=stage.name,
107+
target_step_name=target.name,
108+
decision_history=decision_history_json,
109+
current_required_step_info=required_step_info,
110+
rag_context=rag_context_text,
111+
)
112+
113+
logger.info(
114+
json.dumps(
115+
{
116+
"event": "side_panel_generator_invoke",
117+
"project_id": project.project_id,
118+
"stage_number": stage.stage_number,
119+
"target_step_name": target.name,
120+
"doj_chunks": len(doj_chunks),
121+
"custom_chunks": len(custom_chunks),
122+
"has_required_step": input_data.current_required_step is not None,
123+
"prompt_len": len(prompt),
124+
},
125+
ensure_ascii=False,
126+
)
127+
)
128+
129+
return await self.llm.invoke(prompt, SidePanelOutput)

0 commit comments

Comments
 (0)