soyailabs / app /database.py
GitHub Actions
Auto-deploy from GitHub Actions - 2025-12-11 09:26:27
c7e9f96
raw
history blame
17 kB
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
db = SQLAlchemy()
# ์‚ฌ์šฉ์ž ๋ชจ๋ธ
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
nickname = db.Column(db.String(80), nullable=True) # ๋‹‰๋„ค์ž„ ํ•„๋“œ ์ถ”๊ฐ€
password_hash = db.Column(db.String(255), nullable=False)
is_admin = db.Column(db.Boolean, default=False, nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
last_login = db.Column(db.DateTime, nullable=True)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'nickname': self.nickname,
'is_admin': self.is_admin,
'is_active': self.is_active,
'created_at': self.created_at.isoformat() if self.created_at else None,
'last_login': self.last_login.isoformat() if self.last_login else None
}
# ์—…๋กœ๋“œ๋œ ํŒŒ์ผ ์ •๋ณด ๋ชจ๋ธ
class UploadedFile(db.Model):
id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False)
original_filename = db.Column(db.String(255), nullable=False)
file_path = db.Column(db.String(500), nullable=False)
file_size = db.Column(db.Integer, nullable=False)
model_name = db.Column(db.String(100), nullable=True) # ์—ฐ๊ฒฐ๋œ ๋ชจ๋ธ ์ด๋ฆ„
is_public = db.Column(db.Boolean, default=False, nullable=False) # ๊ณต๊ฐœ ์—ฌ๋ถ€ (๊ธฐ๋ณธ๊ฐ’: ๋ฏธ๊ณต๊ฐœ)
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
uploaded_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
parent_file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=True) # ์ด์–ด์„œ ์—…๋กœ๋“œํ•œ ๊ฒฝ์šฐ ์›๋ณธ ํŒŒ์ผ ID
# ๊ด€๊ณ„
parent_file = db.relationship('UploadedFile', remote_side=[id], backref='child_files')
def to_dict(self):
# ์ฒญํฌ ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ
chunk_count = len(self.chunks) if hasattr(self, 'chunks') else 0
return {
'id': self.id,
'filename': self.filename,
'original_filename': self.original_filename,
'file_size': self.file_size,
'model_name': self.model_name,
'is_public': self.is_public,
'uploaded_at': self.uploaded_at.isoformat() if self.uploaded_at else None,
'uploaded_by': self.uploaded_by,
'parent_file_id': self.parent_file_id,
'chunk_count': chunk_count,
'child_count': len(self.child_files) if self.child_files else 0
}
# ๋Œ€ํ™” ์„ธ์…˜ ๋ชจ๋ธ
class ChatSession(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
title = db.Column(db.String(255), nullable=True)
model_name = db.Column(db.String(100), nullable=True) # ํ•˜์œ„ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด ์œ ์ง€
analysis_model = db.Column(db.String(100), nullable=True) # ์งˆ๋ฌธ ๋ถ„์„์šฉ ๋ชจ๋ธ
answer_model = db.Column(db.String(100), nullable=True) # ์ตœ์ข… ๋‹ต๋ณ€์šฉ ๋ชจ๋ธ
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# ๊ด€๊ณ„
user = db.relationship('User', backref='chat_sessions')
messages = db.relationship('ChatMessage', backref='session', lazy=True, cascade='all, delete-orphan', order_by='ChatMessage.created_at')
def to_dict(self):
return {
'id': self.id,
'user_id': self.user_id,
'title': self.title,
'model_name': self.model_name, # ํ•˜์œ„ ํ˜ธํ™˜์„ฑ
'analysis_model': self.analysis_model,
'answer_model': self.answer_model,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
'message_count': len(self.messages)
}
# ๋Œ€ํ™” ๋ฉ”์‹œ์ง€ ๋ชจ๋ธ
class ChatMessage(db.Model):
id = db.Column(db.Integer, primary_key=True)
session_id = db.Column(db.Integer, db.ForeignKey('chat_session.id'), nullable=True) # ์‹œ์Šคํ…œ ์‚ฌ์šฉ์€ NULL ํ—ˆ์šฉ
role = db.Column(db.String(20), nullable=False) # 'user' or 'ai'
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
# ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ ์ •๋ณด (AI ์‘๋‹ต ๋ฉ”์‹œ์ง€์—๋งŒ ์ €์žฅ)
input_tokens = db.Column(db.Integer, nullable=True) # ์ž…๋ ฅ ํ† ํฐ ์ˆ˜
output_tokens = db.Column(db.Integer, nullable=True) # ์ถœ๋ ฅ ํ† ํฐ ์ˆ˜
model_name = db.Column(db.String(100), nullable=True) # ์‚ฌ์šฉ๋œ AI ๋ชจ๋ธ๋ช…
usage_type = db.Column(db.String(20), nullable=True, default='user') # 'user' or 'system' (์‹œ์Šคํ…œ ์‚ฌ์šฉ ๊ตฌ๋ถ„)
def to_dict(self):
return {
'id': self.id,
'session_id': self.session_id,
'role': self.role,
'content': self.content,
'created_at': self.created_at.isoformat() if self.created_at else None,
'input_tokens': self.input_tokens,
'output_tokens': self.output_tokens,
'model_name': self.model_name,
'usage_type': self.usage_type
}
# ๋ฌธ์„œ ์ฒญํฌ ๋ชจ๋ธ (RAG์šฉ)
class DocumentChunk(db.Model):
id = db.Column(db.Integer, primary_key=True)
file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=False)
chunk_index = db.Column(db.Integer, nullable=False) # ์ฒญํฌ ์ˆœ์„œ
content = db.Column(db.Text, nullable=False) # ์ฒญํฌ ๋‚ด์šฉ
embedding = db.Column(db.Text, nullable=True) # ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ (JSON ๋ฌธ์ž์—ด๋กœ ์ €์žฅ)
chunk_metadata = db.Column(db.Text, nullable=True) # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ (JSON ๋ฌธ์ž์—ด๋กœ ์ €์žฅ: chapter, pov, characters, time_background)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
# ๊ด€๊ณ„
file = db.relationship('UploadedFile', backref='chunks')
def to_dict(self):
import json
metadata_dict = None
if self.chunk_metadata:
try:
metadata_dict = json.loads(self.chunk_metadata)
except:
metadata_dict = None
return {
'id': self.id,
'file_id': self.file_id,
'chunk_index': self.chunk_index,
'content': self.content,
'metadata': metadata_dict,
'created_at': self.created_at.isoformat() if self.created_at else None
}
# Parent Chunk ๋ชจ๋ธ (AI ๋ถ„์„ ๊ฒฐ๊ณผ ์ €์žฅ)
class ParentChunk(db.Model):
id = db.Column(db.Integer, primary_key=True)
file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=False, unique=True)
world_view = db.Column(db.Text, nullable=True) # ์„ธ๊ณ„๊ด€ ์„ค๋ช…
characters = db.Column(db.Text, nullable=True) # ์ฃผ์š” ์บ๋ฆญํ„ฐ ๋ถ„์„
story = db.Column(db.Text, nullable=True) # ์ฃผ์š” ์Šคํ† ๋ฆฌ ๋ถ„์„
episodes = db.Column(db.Text, nullable=True) # ์ฃผ์š” ์—ํ”ผ์†Œ๋“œ ๋ถ„์„
others = db.Column(db.Text, nullable=True) # ๊ธฐํƒ€
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# ๊ด€๊ณ„
file = db.relationship('UploadedFile', backref='parent_chunk')
def to_dict(self):
return {
'id': self.id,
'file_id': self.file_id,
'world_view': self.world_view,
'characters': self.characters,
'story': self.story,
'episodes': self.episodes,
'others': self.others,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
# ์‹œ์Šคํ…œ ์„ค์ • ๋ชจ๋ธ (API ํ‚ค ๋“ฑ)
class SystemConfig(db.Model):
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False) # ์„ค์ • ํ‚ค (์˜ˆ: 'gemini_api_key')
value = db.Column(db.Text, nullable=True) # ์„ค์ • ๊ฐ’ (์•”ํ˜ธํ™”๋œ API ํ‚ค)
description = db.Column(db.String(255), nullable=True) # ์„ค์ • ์„ค๋ช…
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
def to_dict(self):
return {
'id': self.id,
'key': self.key,
'value': self.value, # ์‹ค์ œ ๊ฐ’ ๋ฐ˜ํ™˜ (๋ณด์•ˆ์„ ์œ„ํ•ด ๋งˆ์Šคํ‚น ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ)
'description': self.description,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
@staticmethod
def get_config(key, default=None):
"""์„ค์ • ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ"""
try:
config = SystemConfig.query.filter_by(key=key).first()
return config.value if config else default
except Exception as e:
# ํ…Œ์ด๋ธ”์ด ์—†๊ฑฐ๋‚˜ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜
print(f"[SystemConfig.get_config] ์˜ค๋ฅ˜: {e}")
return default
@staticmethod
def set_config(key, value, description=None):
"""์„ค์ • ๊ฐ’ ์ €์žฅ/์—…๋ฐ์ดํŠธ"""
try:
# ํ…Œ์ด๋ธ”์ด ์—†์œผ๋ฉด ์ƒ์„ฑ ์‹œ๋„
from sqlalchemy import inspect
inspector = inspect(db.engine)
if 'system_config' not in inspector.get_table_names():
print("[SystemConfig.set_config] SystemConfig ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ค‘...")
db.create_all()
config = SystemConfig.query.filter_by(key=key).first()
if config:
print(f"[SystemConfig.set_config] ๊ธฐ์กด ์„ค์ • ์—…๋ฐ์ดํŠธ: {key}")
config.value = value
if description:
config.description = description
config.updated_at = datetime.utcnow()
else:
print(f"[SystemConfig.set_config] ์ƒˆ ์„ค์ • ์ƒ์„ฑ: {key}")
config = SystemConfig(key=key, value=value, description=description)
db.session.add(config)
db.session.commit()
# ์ €์žฅ ํ™•์ธ
verify_config = SystemConfig.query.filter_by(key=key).first()
if verify_config and verify_config.value == value:
print(f"[SystemConfig.set_config] ์„ค์ • ์ €์žฅ ํ™•์ธ๋จ: {key} (๊ธธ์ด: {len(str(value))}์ž)")
else:
print(f"[SystemConfig.set_config] ๊ฒฝ๊ณ : ์„ค์ • ์ €์žฅ ํ›„ ํ™•์ธ ์‹คํŒจ: {key}")
return config
except Exception as e:
db.session.rollback()
print(f"[SystemConfig.set_config] ์˜ค๋ฅ˜: {e}")
import traceback
traceback.print_exc()
raise
# ํšŒ์ฐจ๋ณ„ ๋ถ„์„ ๋ชจ๋ธ
class EpisodeAnalysis(db.Model):
id = db.Column(db.Integer, primary_key=True)
file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=False)
episode_title = db.Column(db.String(100), nullable=False) # ํšŒ์ฐจ ์ œ๋ชฉ (์˜ˆ: '1ํ™”', '2ํ™”')
analysis_content = db.Column(db.Text, nullable=False) # ๋ถ„์„ ๊ฒฐ๊ณผ (ํ•˜๋‚˜์˜ ํ…์ŠคํŠธ๋กœ ์ด์–ด์„œ ์ €์žฅ)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# ๊ด€๊ณ„
file = db.relationship('UploadedFile', backref='episode_analyses')
def to_dict(self):
return {
'id': self.id,
'file_id': self.file_id,
'episode_title': self.episode_title,
'analysis_content': self.analysis_content,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
# Graph Extraction ๋ชจ๋ธ (GraphRAG์šฉ)
class GraphEntity(db.Model):
"""Graph Extraction์—์„œ ์ถ”์ถœ๋œ ์—”ํ‹ฐํ‹ฐ (์ธ๋ฌผ/์žฅ์†Œ)"""
id = db.Column(db.Integer, primary_key=True)
file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=False)
episode_title = db.Column(db.String(100), nullable=False) # ํšŒ์ฐจ ์ œ๋ชฉ
entity_name = db.Column(db.String(200), nullable=False) # ์—”ํ‹ฐํ‹ฐ ์ด๋ฆ„
entity_type = db.Column(db.String(50), nullable=False) # 'character' ๋˜๋Š” 'location'
description = db.Column(db.Text, nullable=True) # ์—”ํ‹ฐํ‹ฐ ์„ค๋ช…
role = db.Column(db.String(200), nullable=True) # ์ธ๋ฌผ์˜ ๊ฒฝ์šฐ ์—ญํ• 
category = db.Column(db.String(200), nullable=True) # ์žฅ์†Œ์˜ ๊ฒฝ์šฐ ์œ ํ˜•
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
# ๊ด€๊ณ„
file = db.relationship('UploadedFile', backref='graph_entities')
def to_dict(self):
return {
'id': self.id,
'file_id': self.file_id,
'episode_title': self.episode_title,
'entity_name': self.entity_name,
'entity_type': self.entity_type,
'description': self.description,
'role': self.role,
'category': self.category,
'created_at': self.created_at.isoformat() if self.created_at else None
}
class GraphRelationship(db.Model):
"""Graph Extraction์—์„œ ์ถ”์ถœ๋œ ๊ด€๊ณ„"""
id = db.Column(db.Integer, primary_key=True)
file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=False)
episode_title = db.Column(db.String(100), nullable=False) # ํšŒ์ฐจ ์ œ๋ชฉ
source = db.Column(db.String(200), nullable=False) # ๊ด€๊ณ„์˜ ์ฃผ์ฒด
target = db.Column(db.String(200), nullable=False) # ๊ด€๊ณ„์˜ ๋Œ€์ƒ
relationship_type = db.Column(db.String(200), nullable=False) # ๊ด€๊ณ„ ์œ ํ˜•
description = db.Column(db.Text, nullable=True) # ๊ด€๊ณ„ ์„ค๋ช…
event = db.Column(db.String(500), nullable=True) # ๊ด€๊ณ„๋ฅผ ํ˜•์„ฑ/๋ณ€ํ™”์‹œํ‚จ ์‚ฌ๊ฑด
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
# ๊ด€๊ณ„
file = db.relationship('UploadedFile', backref='graph_relationships')
def to_dict(self):
return {
'id': self.id,
'file_id': self.file_id,
'episode_title': self.episode_title,
'source': self.source,
'target': self.target,
'relationship_type': self.relationship_type,
'description': self.description,
'event': self.event,
'created_at': self.created_at.isoformat() if self.created_at else None
}
class GraphEvent(db.Model):
"""Graph Extraction์—์„œ ์ถ”์ถœ๋œ ์‚ฌ๊ฑด"""
id = db.Column(db.Integer, primary_key=True)
file_id = db.Column(db.Integer, db.ForeignKey('uploaded_file.id'), nullable=False)
episode_title = db.Column(db.String(100), nullable=False) # ํšŒ์ฐจ ์ œ๋ชฉ
event_name = db.Column(db.String(500), nullable=False) # ์‚ฌ๊ฑด ์ด๋ฆ„
description = db.Column(db.Text, nullable=False) # ์‚ฌ๊ฑด ์„ค๋ช…
participants = db.Column(db.Text, nullable=True) # ๊ด€๋ จ ์ธ๋ฌผ๋“ค (JSON ๋ฌธ์ž์—ด๋กœ ์ €์žฅ)
location = db.Column(db.String(500), nullable=True) # ์‚ฌ๊ฑด ๋ฐœ์ƒ ์žฅ์†Œ
significance = db.Column(db.String(200), nullable=True) # ์‚ฌ๊ฑด์˜ ์ค‘์š”๋„
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
# ๊ด€๊ณ„
file = db.relationship('UploadedFile', backref='graph_events')
def to_dict(self):
import json
participants_list = []
if self.participants:
try:
participants_list = json.loads(self.participants)
except:
participants_list = []
return {
'id': self.id,
'file_id': self.file_id,
'episode_title': self.episode_title,
'event_name': self.event_name,
'description': self.description,
'participants': participants_list,
'location': self.location,
'significance': self.significance,
'created_at': self.created_at.isoformat() if self.created_at else None
}