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: 3 additions & 4 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 = self.llm.invoke(prompt)
strategy = self.parse_analysis_strategy(response.content.strip())
response_text = self.invoke_llm_with_cache(prompt, purpose="analysis", log_label="analysis_strategy")
strategy = self.parse_analysis_strategy(response_text.strip())

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

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

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

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


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

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

Expand All @@ -31,3 +38,22 @@ 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 = self.llm.invoke(prompt)
response_text = self.invoke_llm_with_cache(prompt, purpose="analysis", log_label="confidence_evaluation")

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

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 = self.llm.invoke(prompt)
strategy = self.parse_data_strategy(response.content.strip())
response_text = self.invoke_llm_with_cache(prompt, purpose="analysis", log_label="data_strategy")
strategy = self.parse_data_strategy(response_text.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 = self.llm.invoke(prompt)
result = self.parse_response(response.content.strip())
response_text = self.invoke_llm_with_cache(prompt, purpose="classification", log_label="intent_detection")
result = self.parse_response(response_text.strip())

self.log(f"투자 의도 감지: {result['is_investment_question']} (신뢰도: {result['confidence']:.2f})")
self.log(f" 근거: {result['reasoning']}")
Expand Down
10 changes: 4 additions & 6 deletions app/services/langgraph_enhanced/agents/knowledge_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,8 @@ def process(self, user_query: str, query_analysis: Dict[str, Any]) -> Dict[str,
required_services=query_analysis.get('required_services', [])
)

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

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

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

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

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

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

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

return {
'success': True,
Expand Down
124 changes: 58 additions & 66 deletions app/services/langgraph_enhanced/agents/news_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,87 +194,79 @@ def _format_news_data(self, news_data: List[Dict[str, Any]]) -> str:
return "\n".join(formatted)

async def process(self, user_query: str, query_analysis: Dict[str, Any]) -> Dict[str, Any]:
"""뉴스 에이전트 처리 (async)"""
"""뉴스 에이전트 처리 (async)
Fast-path: 단순 뉴스 질의는 전략 LLM/분석 LLM 생략하고 news_service 직접 호출(10s 타임박스) + 간단 요약 반환
"""
try:
self.log(f"뉴스 수집 시작: {user_query}")

# LLM이 뉴스 수집 전략 결정
primary = query_analysis.get('primary_intent', 'news')
complexity = query_analysis.get('complexity_level', 'simple')
is_simple_news = (primary == 'news' and complexity == 'simple')

import asyncio
news_data: List[Dict[str, Any]] = []
mk_context = ""
strategy: Dict[str, Any] = {}

if is_simple_news:
# Fast-path: news_service 직접 호출 + 타임박스 10s
try:
news_data = await asyncio.wait_for(
news_service.get_comprehensive_news(query=user_query, translate=True),
timeout=10.0
)
except asyncio.TimeoutError:
self.log("뉴스 수집 타임아웃(10s)")
news_data = []

# 간단 요약(LLM 미사용)
if news_data:
lines = []
for i, n in enumerate(news_data[:3], 1):
title = n.get('title', '제목 없음')
src = n.get('source', 'N/A')
pub = n.get('published', '')[:10]
lines.append(f"{i}. {title} (출처: {src}, {pub})")
analysis_result = "\n".join(lines)
else:
analysis_result = "관련 뉴스를 찾을 수 없습니다."

return {
'success': True,
'news_data': news_data,
'analysis_result': analysis_result,
'strategy': {},
'fast_path': True
}

# 일반 경로: 기존 전략 + 분석(단, 캐시/로깅 적용)
prompt = self.get_prompt_template().format(
user_query=user_query,
primary_intent=query_analysis.get('primary_intent', 'news'),
complexity_level=query_analysis.get('complexity_level', 'simple'),
required_services=query_analysis.get('required_services', [])
)

response = self.llm.invoke(prompt)
strategy = self.parse_news_strategy(response.content.strip())

print(f"🔍 [NewsAgent] 생성된 전략:")
print(f" - search_strategy: {strategy.get('search_strategy')}")
print(f" - search_query: {strategy.get('search_query')}")
print(f" - news_sources: {strategy.get('news_sources')}")

# 실제 뉴스 수집 (async)
news_data = []
mk_context = "" # 매일경제 컨텍스트는 별도로 저장

try:
if strategy['news_sources'] in ['google', 'both']:
print(f"📰 [NewsAgent] Google RSS에서 뉴스 수집 시작: {strategy['search_query']}")
# async 함수 직접 호출 - 리스트 반환
google_news = await news_service.get_comprehensive_news(
query=strategy['search_query']
)

print(f" ✅ [NewsAgent] Google RSS 결과: {len(google_news) if google_news else 0}개")

if google_news and isinstance(google_news, list):
news_data.extend(google_news)

if strategy['news_sources'] in ['mk', 'both']:
# 매일경제 KG 컨텍스트는 한국어 핵심 키워드 사용
# 예: "금리 뉴스 분석해줘" → "금리"
korean_keyword = self._extract_korean_keyword(user_query)
print(f" 📚 [NewsAgent] 매일경제 KG 검색 키워드: {korean_keyword}")

# async 함수 호출 - 문자열 반환
mk_context = await news_service.get_analysis_context_from_kg(
query=korean_keyword,
limit=5
)

# 중복 제거 및 정렬
news_data = self._deduplicate_news(news_data)

except Exception as e:
self.log(f"뉴스 수집 오류: {e}")
import traceback
traceback.print_exc()
news_data = []
mk_context = ""

# 뉴스 분석
if news_data or mk_context:
response_text = self.invoke_llm_with_cache(prompt, purpose="news", log_label="news_strategy")
strategy = self.parse_news_strategy(response_text.strip())

# 실제 뉴스 수집
news_data = await news_service.get_comprehensive_news(
query=strategy.get('search_query') or user_query,
translate=True
)

# 간단 분석(짧은 요약)으로 토큰 최소화
if news_data:
analysis_prompt = self.generate_news_analysis_prompt(news_data, strategy, user_query)

# 매일경제 컨텍스트 추가
if mk_context:
analysis_prompt += f"\n\n{mk_context}"

analysis_response = self.llm.invoke(analysis_prompt)
analysis_result = analysis_response.content

self.log(f"뉴스 분석 완료: {len(news_data or [])}건")
analysis_result = self.invoke_llm_with_cache(analysis_prompt, purpose="news", log_label="news_analysis_short")
else:
analysis_result = "관련 뉴스를 찾을 수 없습니다. 다른 키워드로 검색해보세요."
self.log("뉴스를 찾을 수 없음")


return {
'success': True,
'news_data': news_data,
'analysis_result': analysis_result,
'strategy': strategy,
'mk_context': mk_context
'strategy': strategy
}

except Exception as e:
Expand Down
8 changes: 4 additions & 4 deletions app/services/langgraph_enhanced/agents/query_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ def process(self, user_query: str) -> Dict[str, Any]:

# 2. 일반 쿼리 분석
prompt = self.get_prompt_template().format(user_query=user_query)
response = self.llm.invoke(prompt)
analysis_result = self.parse_response(response.content.strip())
response_text = self.invoke_llm_with_cache(prompt, purpose="classification", log_label="query_analysis")
analysis_result = self.parse_response(response_text.strip())

# 3. 투자 의도 정보 통합
analysis_result['is_investment_question'] = is_investment_question
Expand Down Expand Up @@ -251,8 +251,8 @@ def process(self, user_query: str) -> Dict[str, Any]:

# 2. 일반 쿼리 분석
prompt = self.get_prompt_template().format(user_query=user_query)
response = self.llm.invoke(prompt)
analysis_result = self.parse_response(response.content.strip())
response_text = self.invoke_llm_with_cache(prompt, purpose="classification", log_label="query_analysis_dup")
analysis_result = self.parse_response(response_text.strip())

# 3. 투자 의도 정보 통합
analysis_result['is_investment_question'] = is_investment_question
Expand Down
3 changes: 1 addition & 2 deletions app/services/langgraph_enhanced/agents/response_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,7 @@ def process(self, user_query: str, query_analysis: Dict[str, Any], collected_dat
collected_information=collected_info
)

response = self.llm.invoke(prompt)
final_response = response.content
final_response = self.invoke_llm_with_cache(prompt, purpose="response", log_label="final_response_generation")

self.log("최종 응답 생성 완료")

Expand Down
3 changes: 1 addition & 2 deletions app/services/langgraph_enhanced/agents/result_combiner.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,7 @@ def process(
)

# LLM 호출
response = self.llm.invoke(prompt)
combined_response = response.content
combined_response = self.invoke_llm_with_cache(prompt, purpose="response", log_label="result_combination")

# 신뢰도 추출
confidence = self._extract_confidence(combined_response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,7 @@ def process(self, user_query: str, query_analysis: Dict[str, Any]) -> Dict[str,
# 차트 분석
if chart_data and 'error' not in chart_data and strategy.get('include_analysis', True):
analysis_prompt = self.generate_chart_analysis_prompt(chart_data, strategy, user_query)
analysis_response = self.llm.invoke(analysis_prompt)
analysis_result = analysis_response.content
analysis_result = self.invoke_llm_with_cache(analysis_prompt, purpose="analysis", log_label="chart_analysis")

self.log("차트 분석 완료")
else:
Expand Down
Loading