Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app/services/langgraph_enhanced/agents/analysis_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ async def process(self, user_query: str, query_analysis: Dict[str, Any]) -> Dict
required_services=query_analysis.get('required_services', [])
)

response_text = self.invoke_llm_with_cache(prompt, purpose="analysis", log_label="analysis_strategy")
strategy = self.parse_analysis_strategy(response_text.strip())
response = self.llm.invoke(prompt)
strategy = self.parse_analysis_strategy(response.content.strip())

# 종목명 추출
stock_symbol = self._extract_stock_symbol(user_query)
Expand Down Expand Up @@ -413,7 +413,8 @@ async def process(self, user_query: str, query_analysis: Dict[str, Any]) -> Dict
✅ **균형성**: 호재와 악재의 **영향력을 비교 분석**하여 종합적인 판단 제시
✅ **실용성**: 실제 투자에 바로 활용 가능한 구체적 전략 제시"""

analysis_result = self.invoke_llm_with_cache(analysis_prompt, purpose="analysis", log_label="integrated_investment_analysis")
analysis_response = self.llm.invoke(analysis_prompt)
analysis_result = analysis_response.content

self.log(f"통합 투자 분석 완료: {stock_symbol or stock_name}")
else:
Expand Down
32 changes: 3 additions & 29 deletions app/services/langgraph_enhanced/agents/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,15 @@

from abc import ABC, abstractmethod
from typing import Dict, Any
import time
try:
# 실제 실행 환경에서는 전역 llm_manager를 사용
from ..llm_manager import llm_manager as _global_llm_manager
except Exception:
# 테스트/데모 환경에서 외부 의존성 없이 임시 매니저를 주입 가능
_global_llm_manager = None
from ..llm_manager import LLMManager


class BaseAgent(ABC):
"""기본 에이전트 클래스"""

def __init__(self, purpose: str = "general"):
# 전역 LLM 매니저 공유 (캐시 공유). 테스트 환경에서는 외부 의존성 없이 주입 가능
self.llm_manager = _global_llm_manager
self.llm = self.llm_manager.get_llm(purpose=purpose) if self.llm_manager else None
self.llm_manager = LLMManager()
self.llm = self.llm_manager.get_llm(purpose=purpose)
self.purpose = purpose
self.agent_name = ""

Expand All @@ -38,22 +31,3 @@ def log(self, message: str):
"""로그 출력"""
print(f"🤖 {self.agent_name}: {message}")

def invoke_llm_with_cache(self, prompt: str, purpose: str = None, log_label: str = None) -> str:
"""LLM 호출(캐시 적용) + 실행 시간 로깅 공통 헬퍼"""
label = log_label or "llm_invoke"
start = time.time()
print(f"⏳ [{self.agent_name}] {label} 시작")
try:
response_text = self.llm_manager.invoke_with_cache(
self.llm,
prompt,
purpose=(purpose or self.purpose)
)
elapsed = (time.time() - start) * 1000
print(f"✅ [{self.agent_name}] {label} 완료 - {elapsed:.1f}ms")
return response_text
except Exception as e:
elapsed = (time.time() - start) * 1000
print(f"❌ [{self.agent_name}] {label} 실패 - {elapsed:.1f}ms - {e}")
raise

Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ def process(
)

# LLM 호출
response_text = self.invoke_llm_with_cache(prompt, purpose="analysis", log_label="confidence_evaluation")
response = self.llm.invoke(prompt)

# 응답 파싱
evaluation = self.parse_response(response_text)
evaluation = self.parse_response(response.content)

print(f"📊 신뢰도 평가 완료:")
print(f" 전체 신뢰도: {evaluation['overall_confidence']:.2f}")
Expand Down
14 changes: 7 additions & 7 deletions app/services/langgraph_enhanced/agents/data_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,22 @@ def get_prompt_template(self) -> str:
**Yahoo Finance에서 사용하는 정확한 심볼**을 data_query에 입력하세요.

### 변환 규칙:
1. 한국 주식: 6자리 코드 + `.KS`
1. **한국 주식**: 6자리 코드 + `.KS`
- 예: 삼성전자 → 005930.KS, 네이버 → 035420.KS

2. 미국 주식: 표준 티커 심볼 (1~5자 알파벳)
2. **미국 주식**: 표준 티커 심볼 (1~5자 알파벳)
- 예: 테슬라 → TSLA, 애플 → AAPL, 디즈니 → DIS, 스타벅스 → SBUX, 나이키 → NKE
- **당신의 금융 지식을 활용하여 모든 회사명을 정확한 티커 심볼로 변환하세요**

3. 유럽 주식: 티커 + 거래소 접미사
3. **유럽 주식**: 티커 + 거래소 접미사
- 프랑스 (파리): `.PA` (예: LVMH → MC.PA, 에르메스 → RMS.PA)
- 영국 (런던): `.L` (예: BP → BP.L)
- 독일 (프랑크푸르트): `.DE` (예: BMW → BMW.DE)

4. 이미 심볼 형태인 경우: 그대로 사용
4. **이미 심볼 형태**인 경우: 그대로 사용
- 예: "TSLA 주가" → TSLA, "DIS 차트" → DIS

중요:
**중요**:
- 회사명(한글/영어)을 받으면 반드시 Yahoo Finance 티커 심볼로 변환하세요
- 개별 상장되지 않은 브랜드(예: 구찌)는 모기업 심볼(Kering)을 사용하거나 "상장되지 않음" 안내

Expand Down Expand Up @@ -199,8 +199,8 @@ def process(self, user_query: str, query_analysis: Dict[str, Any]) -> Dict[str,
required_services=query_analysis.get('required_services', [])
)

response_text = self.invoke_llm_with_cache(prompt, purpose="analysis", log_label="data_strategy")
strategy = self.parse_data_strategy(response_text.strip())
response = self.llm.invoke(prompt)
strategy = self.parse_data_strategy(response.content.strip())

# 실제 데이터 조회
data = financial_data_service.get_financial_data(strategy['data_query'])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ def detect(self, user_query: str) -> Dict[str, Any]:
"""투자 의도 감지"""
try:
prompt = self.get_prompt_template().format(user_query=user_query)
response_text = self.invoke_llm_with_cache(prompt, purpose="classification", log_label="intent_detection")
result = self.parse_response(response_text.strip())
response = self.llm.invoke(prompt)
result = self.parse_response(response.content.strip())

self.log(f"투자 의도 감지: {result['is_investment_question']} (신뢰도: {result['confidence']:.2f})")
self.log(f" 근거: {result['reasoning']}")
Expand Down
107 changes: 101 additions & 6 deletions app/services/langgraph_enhanced/agents/knowledge_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,12 +403,105 @@ def _get_rag_context(self, user_query: str, namespace: str, top_k: int = 5) -> s
self.log(f"RAG 검색 오류: {e}")
return ""

def _get_simple_knowledge_response(self, user_query: str) -> str:
"""Fast-path 지식 응답: 간단한 질문에 대한 빠른 답변"""
try:
# 1. 내장 지식 DB에서 직접 검색
knowledge_db = self._load_knowledge_database()

# 키워드 기반 매칭
query_lower = user_query.lower()
matched_concept = None

for concept, data in knowledge_db.items():
concept_name = data.get('name', '').lower()
if concept.lower() in query_lower or concept_name in query_lower:
matched_concept = concept
break

if matched_concept:
knowledge_data = knowledge_db[matched_concept]
print(f"⚡ Fast-path 지식 매칭: {matched_concept}")

# 간단한 설명 생성
response_parts = [f"📚 **{knowledge_data.get('name', matched_concept)}**\n"]
response_parts.append(f"💡 **정의**: {knowledge_data.get('definition', '정의 없음')}")

if 'formula' in knowledge_data:
response_parts.append(f"📊 **공식**: {knowledge_data['formula']}")

if 'examples' in knowledge_data and knowledge_data['examples']:
response_parts.append(f"📝 **예시**: {knowledge_data['examples'][0]}")

if 'usage' in knowledge_data:
response_parts.append(f"🎯 **활용**: {knowledge_data['usage']}")

return "\n".join(response_parts)

# 2. RAG로 빠른 검색
namespace = self._determine_namespace_simple(user_query)
rag_context = self._get_rag_context(user_query, namespace, top_k=3)

if rag_context:
print(f"⚡ Fast-path RAG 검색: {namespace}")
# 간단한 LLM 호출로 요약
summary_prompt = f"""다음 지식 정보를 바탕으로 사용자 질문에 간단히 답변해주세요.

질문: {user_query}

지식 정보:
{rag_context}

간단하고 명확한 답변을 3-4문장으로 작성해주세요."""

response = self.llm.invoke(summary_prompt)
return response.content.strip()

return "해당 질문에 대한 정보를 찾을 수 없습니다."

except Exception as e:
print(f"⚠️ Fast-path 지식 검색 실패: {e}")
return ""

def _determine_namespace_simple(self, user_query: str) -> str:
"""간단한 네임스페이스 결정 (키워드 기반)"""
query_lower = user_query.lower()

# 키워드 기반 분류
if any(word in query_lower for word in ['뭐야', '무엇', '의미', '정의', '개념', '란', '이란']):
return 'terminology'
elif any(word in query_lower for word in ['분석', '재무', '경제', '실적', '지표']):
return 'financial_analysis'
elif any(word in query_lower for word in ['청년', '정책', '지원', '혜택']):
return 'youth_policy'
elif any(word in query_lower for word in ['투자', '전략', '포트폴리오', '분산']):
return 'investment_strategy'
else:
return 'terminology' # 기본값

def process(self, user_query: str, query_analysis: Dict[str, Any]) -> Dict[str, Any]:
"""지식 에이전트 처리 (네임스페이스 라우팅)"""
"""지식 에이전트 처리 (네임스페이스 라우팅) - Fast-path 지원"""
try:
self.log(f"지식 교육 시작: {user_query}")

# 1. 네임스페이스 결정
# Fast-path 판정: 단순 지식 질의
primary_intent = query_analysis.get('primary_intent', 'knowledge')
complexity = query_analysis.get('complexity_level', 'simple')
is_simple_knowledge = (primary_intent == 'knowledge' and complexity == 'simple')

if is_simple_knowledge:
print("⚡ Knowledge Fast-path: 단순 지식 질의 감지 - 전략 LLM 생략")
# Fast-path: 바로 지식 검색 및 간단한 설명
simple_response = self._get_simple_knowledge_response(user_query)
if simple_response:
return {
'success': True,
'explanation_result': simple_response,
'fast_path': True,
'skip_result_combiner': True # 결과 통합 건너뛰기 플래그
}

# 일반 경로: 1. 네임스페이스 결정
namespace = self._determine_namespace(user_query, query_analysis)

# 2. RAG 컨텍스트 가져오기
Expand All @@ -422,8 +515,8 @@ def process(self, user_query: str, query_analysis: Dict[str, Any]) -> Dict[str,
required_services=query_analysis.get('required_services', [])
)

response_text = self.invoke_llm_with_cache(prompt, purpose="knowledge", log_label="knowledge_strategy")
strategy = self.parse_education_strategy(response_text.strip())
response = self.llm.invoke(prompt)
strategy = self.parse_education_strategy(response.content.strip())

# 4. 설명 생성
if rag_context:
Expand All @@ -450,7 +543,8 @@ def process(self, user_query: str, query_analysis: Dict[str, Any]) -> Dict[str,

명확하고 구체적으로 설명해주세요."""

explanation_result = self.invoke_llm_with_cache(explanation_prompt, purpose="knowledge", log_label="knowledge_explanation_rag")
explanation_response = self.llm.invoke(explanation_prompt)
explanation_result = explanation_response.content

self.log(f"RAG 기반 지식 교육 완료")
else:
Expand All @@ -468,7 +562,8 @@ def process(self, user_query: str, query_analysis: Dict[str, Any]) -> Dict[str,

명확하고 친절하게 설명해주세요."""

explanation_result = self.invoke_llm_with_cache(explanation_prompt, purpose="knowledge", log_label="knowledge_explanation_basic")
explanation_response = self.llm.invoke(explanation_prompt)
explanation_result = explanation_response.content

return {
'success': True,
Expand Down
Loading