feat: clawdbot 클라이언트 추가 및 DB/앱 업데이트
- clawdbot_client.py: 챗봇 연동 클라이언트 - db_setup.py, pet_recommend_app.py 수정 - .gitignore: _dev_scripts/ 제외 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,21 +8,34 @@
|
||||
from flask import Flask, render_template_string, request, jsonify
|
||||
from db_setup import (
|
||||
session, APDB, Inventory, ComponentCode, Symptoms, SymptomComponentMapping, DosageInfo,
|
||||
SupplementaryProduct, UnifiedProductRecommendation, SymptomSupplementaryRecommendation, InventorySupplementary
|
||||
SupplementaryProduct, UnifiedProductRecommendation, SymptomSupplementaryRecommendation, InventorySupplementary,
|
||||
RecommendationLog, EvidenceReference
|
||||
)
|
||||
from sqlalchemy import distinct, and_, or_
|
||||
from sqlalchemy import distinct, and_, or_, desc
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import openai
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
app = Flask(__name__)
|
||||
# AI 설정: OpenAI (즉시응답) + Clawdbot (백그라운드 심층분석)
|
||||
import openai
|
||||
import threading
|
||||
|
||||
# ============================================================
|
||||
# OpenAI API 설정
|
||||
# ============================================================
|
||||
OPENAI_API_KEY = "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"
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
|
||||
# Clawdbot (백그라운드 Opus 분석용)
|
||||
try:
|
||||
from clawdbot_client import ask_clawdbot
|
||||
CLAWDBOT_AVAILABLE = True
|
||||
except ImportError:
|
||||
ask_clawdbot = None
|
||||
CLAWDBOT_AVAILABLE = False
|
||||
print("[WARNING] clawdbot_client 모듈 없음. 백그라운드 Opus 분석 불가.")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# ============================================================
|
||||
# MDR-1 유전자 변이 관련 설정
|
||||
# ============================================================
|
||||
@@ -303,7 +316,7 @@ def calculate_recommended_dosage(product_idx, weight_kg, animal_type):
|
||||
|
||||
|
||||
def generate_recommendation_reason(animal_type, symptom_descriptions, product_name, component_name, llm_pharm, efficacy_clean, component_code=None, symptom_codes=None):
|
||||
"""GPT-4o-mini로 추천 이유 생성"""
|
||||
"""OpenAI GPT-4o-mini로 즉시 추천 이유 생성 (빠른 응답)"""
|
||||
try:
|
||||
# 한글 동물 타입
|
||||
animal_ko = '강아지' if animal_type == 'dog' else '고양이'
|
||||
@@ -368,6 +381,7 @@ def generate_recommendation_reason(animal_type, symptom_descriptions, product_na
|
||||
# 복합 성분 제품은 더 긴 응답 허용
|
||||
max_tok = 280 if is_complex_formula else 120
|
||||
|
||||
# OpenAI GPT-4o-mini (빠른 응답)
|
||||
client = openai.OpenAI(api_key=OPENAI_API_KEY)
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
@@ -2608,13 +2622,49 @@ def recommend():
|
||||
|
||||
unique_recommendations = weight_filtered_recommendations
|
||||
|
||||
# 응답에서 불필요한 필드 제거
|
||||
# 응답에서 불필요한 필드 제거 (로그 저장용 데이터 먼저 추출)
|
||||
log_products = []
|
||||
for rec in unique_recommendations:
|
||||
log_products.append({
|
||||
'product_name': rec.get('name'), # 키가 'name'임
|
||||
'component_name': rec.get('component_name'),
|
||||
'reason': rec.get('reason', '')[:500] # 로그용 요약
|
||||
})
|
||||
rec.pop('llm_pharm', None)
|
||||
rec.pop('efficacy_clean', None)
|
||||
rec.pop('component_code', None)
|
||||
rec.pop('product_idx', None) # 용량 계산 후 제거
|
||||
|
||||
# === 로그 저장 ===
|
||||
try:
|
||||
log_entry = RecommendationLog(
|
||||
session_id=str(uuid.uuid4())[:8],
|
||||
animal_type=animal_type,
|
||||
breed=breed,
|
||||
weight_kg=weight_kg,
|
||||
pregnancy_status=pregnancy,
|
||||
symptoms=symptom_codes,
|
||||
symptom_descriptions=symptom_descriptions,
|
||||
matched_products=log_products,
|
||||
product_count=len(unique_recommendations),
|
||||
gpt_response=log_products[0].get('reason') if log_products else None,
|
||||
client_ip=request.remote_addr,
|
||||
user_agent=request.headers.get('User-Agent', '')[:500]
|
||||
)
|
||||
session.add(log_entry)
|
||||
session.commit()
|
||||
|
||||
# 백그라운드 Opus 분석 트리거 (비동기)
|
||||
if CLAWDBOT_AVAILABLE and log_products:
|
||||
threading.Thread(
|
||||
target=background_opus_analysis,
|
||||
args=(log_entry.id, animal_type, symptom_descriptions, log_products),
|
||||
daemon=True
|
||||
).start()
|
||||
except Exception as log_error:
|
||||
print(f"[LOG] 저장 실패: {log_error}")
|
||||
session.rollback()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'animal_type': animal_type,
|
||||
@@ -2633,6 +2683,57 @@ def recommend():
|
||||
})
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 백그라운드 Opus 분석
|
||||
# ============================================================
|
||||
def background_opus_analysis(log_id, animal_type, symptom_descriptions, products):
|
||||
"""백그라운드에서 Claude Opus로 심층 분석 실행"""
|
||||
try:
|
||||
start_time = time.time()
|
||||
animal_ko = '강아지' if animal_type == 'dog' else '고양이'
|
||||
|
||||
product_list = "\n".join([
|
||||
f"- {p['product_name']} ({p['component_name']})"
|
||||
for p in products[:5] # 최대 5개
|
||||
])
|
||||
|
||||
prompt = f"""반려동물 약사로서 아래 상황을 심층 분석해주세요.
|
||||
|
||||
동물: {animal_ko}
|
||||
증상: {', '.join(symptom_descriptions)}
|
||||
추천된 제품:
|
||||
{product_list}
|
||||
|
||||
다음을 분석해주세요:
|
||||
1. 추천된 제품들의 적합성 평가
|
||||
2. 각 제품의 작용 기전 설명
|
||||
3. 잠재적 부작용/주의사항
|
||||
4. 제품 간 상호작용 여부
|
||||
5. 추가 권장 사항
|
||||
|
||||
전문적이고 상세하게 답변해주세요."""
|
||||
|
||||
opus_response = ask_clawdbot(
|
||||
prompt,
|
||||
session_id=f"opus-analysis-{log_id}",
|
||||
system_prompt="당신은 동물약학 전문가입니다. 심층적이고 근거 기반의 분석을 제공합니다.",
|
||||
timeout=120
|
||||
)
|
||||
|
||||
elapsed_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
# DB 업데이트
|
||||
log_entry = session.query(RecommendationLog).filter_by(id=log_id).first()
|
||||
if log_entry and opus_response:
|
||||
log_entry.opus_response = opus_response
|
||||
log_entry.opus_response_time_ms = elapsed_ms
|
||||
log_entry.opus_analyzed_at = datetime.utcnow()
|
||||
session.commit()
|
||||
print(f"[OPUS] 분석 완료 (log_id={log_id}, {elapsed_ms}ms)")
|
||||
except Exception as e:
|
||||
print(f"[OPUS] 분석 실패: {e}")
|
||||
|
||||
|
||||
@app.route('/api/recommend_by_category', methods=['POST'])
|
||||
def recommend_by_category():
|
||||
"""제품군 기반 추천 API"""
|
||||
@@ -3104,16 +3205,474 @@ def get_symptom_supplementary_recommendations():
|
||||
})
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Admin 페이지 - 추천 로그 조회
|
||||
# ============================================================
|
||||
|
||||
ADMIN_HTML = '''
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>동물약 추천 - 관리자</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.header h1 { font-size: 24px; margin-bottom: 8px; }
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.stat-card h3 { color: #666; font-size: 14px; margin-bottom: 8px; }
|
||||
.stat-card .value { font-size: 32px; font-weight: bold; color: #333; }
|
||||
.stat-card .sub { font-size: 12px; color: #999; margin-top: 4px; }
|
||||
.log-table {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.log-table table { width: 100%; border-collapse: collapse; }
|
||||
.log-table th, .log-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.log-table th { background: #f8f9fa; font-weight: 600; color: #333; }
|
||||
.log-table tr:hover { background: #f8f9fa; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.badge-dog { background: #e3f2fd; color: #1976d2; }
|
||||
.badge-cat { background: #fce4ec; color: #c2185b; }
|
||||
.badge-opus { background: #e8f5e9; color: #388e3c; }
|
||||
.badge-pending { background: #fff3e0; color: #f57c00; }
|
||||
.symptom-tag {
|
||||
display: inline-block;
|
||||
background: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
margin: 2px;
|
||||
}
|
||||
.product-list { font-size: 13px; color: #666; }
|
||||
.expand-btn {
|
||||
background: none;
|
||||
border: 1px solid #ddd;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.expand-btn:hover { background: #f0f0f0; }
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal.active { display: flex; align-items: center; justify-content: center; }
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
width: 90%;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
.comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.comparison-box {
|
||||
background: #f8f9fa;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.comparison-box h4 { margin-bottom: 8px; color: #333; }
|
||||
.comparison-box pre {
|
||||
white-space: pre-wrap;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.refresh-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
.refresh-btn:hover { background: #5a67d8; }
|
||||
.time-ago { color: #999; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🐾 동물약 추천 관리자</h1>
|
||||
<p>추천 조회 로그 및 GPT vs Opus 비교</p>
|
||||
</div>
|
||||
|
||||
<div class="stats" id="stats">
|
||||
<div class="stat-card">
|
||||
<h3>오늘 조회</h3>
|
||||
<div class="value" id="today-count">-</div>
|
||||
<div class="sub">건</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>전체 조회</h3>
|
||||
<div class="value" id="total-count">-</div>
|
||||
<div class="sub">건</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Opus 분석 완료</h3>
|
||||
<div class="value" id="opus-count">-</div>
|
||||
<div class="sub">건</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>강아지 / 고양이</h3>
|
||||
<div class="value" id="animal-ratio">-</div>
|
||||
<div class="sub">비율</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center;">
|
||||
<button class="refresh-btn" onclick="loadLogs()">🔄 새로고침</button>
|
||||
<select id="filterAnimal" onchange="loadLogs()" style="padding: 8px 12px; border-radius: 8px; border: 1px solid #ddd;">
|
||||
<option value="">전체 동물</option>
|
||||
<option value="dog">🐕 강아지</option>
|
||||
<option value="cat">🐱 고양이</option>
|
||||
</select>
|
||||
<select id="filterOpus" onchange="loadLogs()" style="padding: 8px 12px; border-radius: 8px; border: 1px solid #ddd;">
|
||||
<option value="">전체</option>
|
||||
<option value="completed">Opus 완료</option>
|
||||
<option value="pending">Opus 대기중</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="log-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시간</th>
|
||||
<th>동물</th>
|
||||
<th>체중</th>
|
||||
<th>증상</th>
|
||||
<th>추천 제품</th>
|
||||
<th>Opus</th>
|
||||
<th>상세</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="log-body">
|
||||
<tr><td colspan="7" style="text-align:center; padding: 40px;">로딩 중...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="detailModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>📋 상세 정보</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div id="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function timeAgo(dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = Math.floor((now - date) / 1000);
|
||||
|
||||
if (diff < 60) return `${diff}초 전`;
|
||||
if (diff < 3600) return `${Math.floor(diff/60)}분 전`;
|
||||
if (diff < 86400) return `${Math.floor(diff/3600)}시간 전`;
|
||||
return `${Math.floor(diff/86400)}일 전`;
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
const animal = document.getElementById('filterAnimal').value;
|
||||
const opus = document.getElementById('filterOpus').value;
|
||||
|
||||
let url = '/admin/api/logs?limit=50';
|
||||
if (animal) url += `&animal=${animal}`;
|
||||
if (opus) url += `&opus=${opus}`;
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
|
||||
// Stats
|
||||
document.getElementById('today-count').textContent = data.stats.today;
|
||||
document.getElementById('total-count').textContent = data.stats.total;
|
||||
document.getElementById('opus-count').textContent = data.stats.opus_completed;
|
||||
document.getElementById('animal-ratio').textContent =
|
||||
`${data.stats.dog_count} / ${data.stats.cat_count}`;
|
||||
|
||||
// Table
|
||||
const tbody = document.getElementById('log-body');
|
||||
if (data.logs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding: 40px;">조회 로그가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.logs.map(log => `
|
||||
<tr>
|
||||
<td>
|
||||
<div>${new Date(log.created_at).toLocaleString('ko-KR')}</div>
|
||||
<div class="time-ago">${timeAgo(log.created_at)}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-${log.animal_type}">
|
||||
${log.animal_type === 'dog' ? '🐕 강아지' : '🐱 고양이'}
|
||||
</span>
|
||||
${log.breed ? `<div style="font-size:11px; color:#666;">${log.breed}</div>` : ''}
|
||||
</td>
|
||||
<td>${log.weight_kg ? log.weight_kg + 'kg' : '-'}</td>
|
||||
<td>
|
||||
${(log.symptom_descriptions || []).slice(0, 3).map(s =>
|
||||
`<span class="symptom-tag">${s.length > 15 ? s.slice(0,15) + '...' : s}</span>`
|
||||
).join('')}
|
||||
${(log.symptom_descriptions || []).length > 3 ? `<span class="symptom-tag">+${log.symptom_descriptions.length - 3}</span>` : ''}
|
||||
</td>
|
||||
<td class="product-list">
|
||||
${log.product_count}개 추천
|
||||
${log.matched_products && log.matched_products[0] ?
|
||||
`<div style="font-size:11px; color:#333;">${log.matched_products[0].product_name}</div>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
${log.opus_response ?
|
||||
'<span class="badge badge-opus">✓ 완료</span>' :
|
||||
'<span class="badge badge-pending">대기중</span>'}
|
||||
</td>
|
||||
<td>
|
||||
<button class="expand-btn" onclick="showDetail(${log.id})">상세</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function showDetail(logId) {
|
||||
const res = await fetch(`/admin/api/logs/${logId}`);
|
||||
const log = await res.json();
|
||||
|
||||
const modal = document.getElementById('detailModal');
|
||||
const body = document.getElementById('modal-body');
|
||||
|
||||
body.innerHTML = `
|
||||
<div style="margin-bottom: 16px;">
|
||||
<strong>입력 정보:</strong>
|
||||
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; margin-top: 8px;">
|
||||
동물: ${log.animal_type === 'dog' ? '🐕 강아지' : '🐱 고양이'}
|
||||
${log.breed ? ` (${log.breed})` : ''}<br>
|
||||
체중: ${log.weight_kg || '-'}kg<br>
|
||||
증상: ${(log.symptom_descriptions || []).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 16px;">
|
||||
<strong>추천된 제품 (${log.product_count}개):</strong>
|
||||
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; margin-top: 8px;">
|
||||
${(log.matched_products || []).map(p =>
|
||||
`<div style="margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #eee;">
|
||||
<strong>${p.product_name}</strong> (${p.component_name})<br>
|
||||
<span style="font-size: 13px; color: #666;">${p.reason || ''}</span>
|
||||
</div>`
|
||||
).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison">
|
||||
<div class="comparison-box">
|
||||
<h4>💬 GPT-4o-mini (즉시 응답)</h4>
|
||||
<pre>${log.gpt_response || '(없음)'}</pre>
|
||||
${log.gpt_response_time_ms ? `<div style="margin-top: 8px; font-size: 12px; color: #999;">${log.gpt_response_time_ms}ms</div>` : ''}
|
||||
</div>
|
||||
<div class="comparison-box">
|
||||
<h4>🔬 Claude Opus (심층 분석)</h4>
|
||||
<pre>${log.opus_response || '(분석 대기중...)'}</pre>
|
||||
${log.opus_response_time_ms ? `<div style="margin-top: 8px; font-size: 12px; color: #999;">${log.opus_response_time_ms}ms</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${log.evidence_references ? `
|
||||
<div style="margin-top: 16px;">
|
||||
<strong>📚 근거 자료:</strong>
|
||||
<div style="background: #f8f9fa; padding: 12px; border-radius: 8px; margin-top: 8px;">
|
||||
${JSON.stringify(log.evidence_references, null, 2)}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('detailModal').classList.remove('active');
|
||||
}
|
||||
|
||||
// 초기 로드
|
||||
loadLogs();
|
||||
|
||||
// 30초마다 자동 새로고침
|
||||
setInterval(loadLogs, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
@app.route('/admin')
|
||||
def admin_page():
|
||||
"""관리자 페이지"""
|
||||
return ADMIN_HTML
|
||||
|
||||
|
||||
@app.route('/admin/api/logs')
|
||||
def admin_get_logs():
|
||||
"""관리자 API - 로그 조회"""
|
||||
try:
|
||||
limit = request.args.get('limit', 50, type=int)
|
||||
animal_filter = request.args.get('animal', '')
|
||||
opus_filter = request.args.get('opus', '')
|
||||
|
||||
query = session.query(RecommendationLog).order_by(desc(RecommendationLog.created_at))
|
||||
|
||||
if animal_filter:
|
||||
query = query.filter(RecommendationLog.animal_type == animal_filter)
|
||||
|
||||
if opus_filter == 'completed':
|
||||
query = query.filter(RecommendationLog.opus_response.isnot(None))
|
||||
elif opus_filter == 'pending':
|
||||
query = query.filter(RecommendationLog.opus_response.is_(None))
|
||||
|
||||
logs = query.limit(limit).all()
|
||||
|
||||
# 통계
|
||||
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
stats = {
|
||||
'total': session.query(RecommendationLog).count(),
|
||||
'today': session.query(RecommendationLog).filter(RecommendationLog.created_at >= today_start).count(),
|
||||
'opus_completed': session.query(RecommendationLog).filter(RecommendationLog.opus_response.isnot(None)).count(),
|
||||
'dog_count': session.query(RecommendationLog).filter(RecommendationLog.animal_type == 'dog').count(),
|
||||
'cat_count': session.query(RecommendationLog).filter(RecommendationLog.animal_type == 'cat').count(),
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'logs': [{
|
||||
'id': log.id,
|
||||
'created_at': log.created_at.isoformat() if log.created_at else None,
|
||||
'animal_type': log.animal_type,
|
||||
'breed': log.breed,
|
||||
'weight_kg': log.weight_kg,
|
||||
'symptom_descriptions': log.symptom_descriptions,
|
||||
'matched_products': log.matched_products,
|
||||
'product_count': log.product_count,
|
||||
'opus_response': log.opus_response[:100] + '...' if log.opus_response and len(log.opus_response) > 100 else log.opus_response,
|
||||
} for log in logs],
|
||||
'stats': stats
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Admin logs error: {e}")
|
||||
return jsonify({'logs': [], 'stats': {}})
|
||||
|
||||
|
||||
@app.route('/admin/api/logs/<int:log_id>')
|
||||
def admin_get_log_detail(log_id):
|
||||
"""관리자 API - 로그 상세 조회"""
|
||||
try:
|
||||
log = session.query(RecommendationLog).filter_by(id=log_id).first()
|
||||
if not log:
|
||||
return jsonify({'error': 'Not found'}), 404
|
||||
|
||||
return jsonify({
|
||||
'id': log.id,
|
||||
'created_at': log.created_at.isoformat() if log.created_at else None,
|
||||
'animal_type': log.animal_type,
|
||||
'breed': log.breed,
|
||||
'weight_kg': log.weight_kg,
|
||||
'pregnancy_status': log.pregnancy_status,
|
||||
'symptoms': log.symptoms,
|
||||
'symptom_descriptions': log.symptom_descriptions,
|
||||
'matched_products': log.matched_products,
|
||||
'product_count': log.product_count,
|
||||
'gpt_response': log.gpt_response,
|
||||
'gpt_response_time_ms': log.gpt_response_time_ms,
|
||||
'opus_response': log.opus_response,
|
||||
'opus_response_time_ms': log.opus_response_time_ms,
|
||||
'opus_analyzed_at': log.opus_analyzed_at.isoformat() if log.opus_analyzed_at else None,
|
||||
'evidence_references': log.evidence_references,
|
||||
'client_ip': log.client_ip,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Admin log detail error: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 실행
|
||||
# ============================================================
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
print("=" * 60)
|
||||
print("🐾 동물약 추천 MVP 웹앱")
|
||||
print("[PET] 동물약 추천 MVP 웹앱")
|
||||
print("=" * 60)
|
||||
print("접속 주소: http://localhost:7001")
|
||||
print("접속 주소: http://localhost:7002")
|
||||
print("종료: Ctrl+C")
|
||||
print("=" * 60)
|
||||
|
||||
app.run(host='0.0.0.0', port=7001, debug=True)
|
||||
app.run(host='0.0.0.0', port=7002, debug=False)
|
||||
|
||||
Reference in New Issue
Block a user