Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -31,6 +31,7 @@ except ImportError:
|
|
| 31 |
|
| 32 |
# 환경 변수에서 토큰 가져오기
|
| 33 |
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
|
|
|
|
| 34 |
API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
|
| 35 |
MODEL_ID = "dep89a2fld32mcm"
|
| 36 |
TEST_MODE = os.getenv("TEST_MODE", "false").lower() == "true"
|
|
@@ -40,6 +41,9 @@ if not FRIENDLI_TOKEN and not TEST_MODE:
|
|
| 40 |
logger.warning("FRIENDLI_TOKEN not set and TEST_MODE is false. Application will run in test mode.")
|
| 41 |
TEST_MODE = True
|
| 42 |
|
|
|
|
|
|
|
|
|
|
| 43 |
# 전역 변수
|
| 44 |
conversation_history = []
|
| 45 |
selected_language = "English" # 기본 언어
|
|
@@ -52,8 +56,148 @@ db_lock = threading.Lock()
|
|
| 52 |
WRITER_DRAFT_STAGES = [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48] # 작가 초안
|
| 53 |
WRITER_REVISION_STAGES = [5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47, 50] # 작가 수정본
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
class NovelDatabase:
|
| 56 |
-
"""Novel session management database"""
|
| 57 |
|
| 58 |
@staticmethod
|
| 59 |
def init_db():
|
|
@@ -95,12 +239,39 @@ class NovelDatabase:
|
|
| 95 |
)
|
| 96 |
''')
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
# Create indices
|
| 99 |
cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON stages(session_id)')
|
| 100 |
cursor.execute('CREATE INDEX IF NOT EXISTS idx_stage_number ON stages(stage_number)')
|
|
|
|
| 101 |
|
| 102 |
conn.commit()
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
@staticmethod
|
| 105 |
@contextmanager
|
| 106 |
def get_db():
|
|
@@ -299,10 +470,18 @@ class NovelWritingSystem:
|
|
| 299 |
self.model_id = MODEL_ID
|
| 300 |
self.test_mode = TEST_MODE or not self.token
|
| 301 |
|
|
|
|
|
|
|
|
|
|
| 302 |
if self.test_mode:
|
| 303 |
logger.warning("Running in test mode - no actual API calls will be made.")
|
| 304 |
else:
|
| 305 |
logger.info("Running in production mode with API calls enabled.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
|
| 307 |
# Initialize database
|
| 308 |
NovelDatabase.init_db()
|
|
@@ -318,6 +497,60 @@ class NovelWritingSystem:
|
|
| 318 |
"Content-Type": "application/json"
|
| 319 |
}
|
| 320 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
def create_director_initial_prompt(self, user_query: str, language: str = "English") -> str:
|
| 322 |
"""Director AI initial prompt - Novel planning for 16 writers"""
|
| 323 |
if language == "Korean":
|
|
@@ -957,8 +1190,9 @@ Present a complete 48-page novel integrating all writers' contributions."""
|
|
| 957 |
yield chunk + " "
|
| 958 |
time.sleep(0.02)
|
| 959 |
|
| 960 |
-
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str,
|
| 961 |
-
|
|
|
|
| 962 |
|
| 963 |
if self.test_mode:
|
| 964 |
logger.info(f"Test mode streaming - Role: {role}, Language: {language}")
|
|
@@ -966,6 +1200,22 @@ Present a complete 48-page novel integrating all writers' contributions."""
|
|
| 966 |
yield from self.simulate_streaming(test_response, role)
|
| 967 |
return
|
| 968 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 969 |
# Real API call
|
| 970 |
try:
|
| 971 |
system_prompts = self.get_system_prompts(language)
|
|
@@ -1118,6 +1368,7 @@ Present a complete 48-page novel integrating all writers' contributions."""
|
|
| 1118 |
logger.error(f"Error during streaming: {str(e)}")
|
| 1119 |
yield f"❌ Error occurred: {str(e)}"
|
| 1120 |
|
|
|
|
| 1121 |
def get_system_prompts(self, language: str) -> Dict[str, str]:
|
| 1122 |
"""Get system prompts for all 16 writers with enhanced length requirements"""
|
| 1123 |
if language == "Korean":
|
|
@@ -1340,7 +1591,7 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet
|
|
| 1340 |
def process_novel_stream(self, query: str, language: str = "English",
|
| 1341 |
session_id: Optional[str] = None,
|
| 1342 |
resume_from_stage: int = 0) -> Generator[Tuple[str, List[Dict[str, str]]], None, None]:
|
| 1343 |
-
"""Process novel writing with streaming updates"""
|
| 1344 |
try:
|
| 1345 |
global conversation_history
|
| 1346 |
|
|
@@ -1403,6 +1654,10 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet
|
|
| 1403 |
for stage_idx in range(resume_from_stage, len(stage_definitions)):
|
| 1404 |
role, stage_name = stage_definitions[stage_idx]
|
| 1405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1406 |
# Add stage if not already present
|
| 1407 |
if stage_idx >= len(stages):
|
| 1408 |
stages.append({
|
|
@@ -1418,13 +1673,21 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet
|
|
| 1418 |
# Get appropriate prompt based on stage
|
| 1419 |
prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
|
| 1420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1421 |
stage_content = ""
|
| 1422 |
|
| 1423 |
-
# Stream content generation
|
| 1424 |
for chunk in self.call_llm_streaming(
|
| 1425 |
[{"role": "user", "content": prompt}],
|
| 1426 |
role,
|
| 1427 |
-
language
|
|
|
|
| 1428 |
):
|
| 1429 |
stage_content += chunk
|
| 1430 |
stages[stage_idx]["content"] = stage_content
|
|
@@ -1559,7 +1822,7 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet
|
|
| 1559 |
|
| 1560 |
return ""
|
| 1561 |
|
| 1562 |
-
# Gradio Interface Functions
|
| 1563 |
def process_query(query: str, language: str, session_id: str = None) -> Generator[Tuple[str, str, str], None, None]:
|
| 1564 |
"""Process query and yield updates"""
|
| 1565 |
if not query.strip() and not session_id:
|
|
@@ -1765,6 +2028,11 @@ custom_css = """
|
|
| 1765 |
border-radius: 8px;
|
| 1766 |
margin-top: 20px;
|
| 1767 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1768 |
"""
|
| 1769 |
|
| 1770 |
# Create Gradio Interface
|
|
@@ -1776,11 +2044,12 @@ def create_interface():
|
|
| 1776 |
📚 SOMA Novel Writing System
|
| 1777 |
</h1>
|
| 1778 |
<h3 style="color: #ccc; margin-bottom: 20px;">
|
| 1779 |
-
AI Collaborative Novel Generation - 48 Page Novella Creator
|
| 1780 |
</h3>
|
| 1781 |
<p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
|
| 1782 |
Enter a theme or prompt, and watch as 19 AI agents collaborate to create a complete 48-page novella.
|
| 1783 |
The system includes 1 Director, 1 Critic, and 16 Writers (each writing 3 pages) working in harmony.
|
|
|
|
| 1784 |
All progress is automatically saved and can be resumed anytime.
|
| 1785 |
</p>
|
| 1786 |
</div>
|
|
@@ -1804,6 +2073,11 @@ def create_interface():
|
|
| 1804 |
label="Language / 언어"
|
| 1805 |
)
|
| 1806 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1807 |
with gr.Row():
|
| 1808 |
submit_btn = gr.Button("🚀 Start Writing / 작성 시작", variant="primary", scale=2)
|
| 1809 |
clear_btn = gr.Button("🗑️ Clear / 초기화", scale=1)
|
|
@@ -1979,6 +2253,12 @@ if __name__ == "__main__":
|
|
| 1979 |
else:
|
| 1980 |
logger.info(f"Running in PRODUCTION MODE with API endpoint: {API_URL}")
|
| 1981 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1982 |
# Initialize database on startup
|
| 1983 |
logger.info("Initializing database...")
|
| 1984 |
NovelDatabase.init_db()
|
|
|
|
| 31 |
|
| 32 |
# 환경 변수에서 토큰 가져오기
|
| 33 |
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
|
| 34 |
+
BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
|
| 35 |
API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
|
| 36 |
MODEL_ID = "dep89a2fld32mcm"
|
| 37 |
TEST_MODE = os.getenv("TEST_MODE", "false").lower() == "true"
|
|
|
|
| 41 |
logger.warning("FRIENDLI_TOKEN not set and TEST_MODE is false. Application will run in test mode.")
|
| 42 |
TEST_MODE = True
|
| 43 |
|
| 44 |
+
if not BRAVE_SEARCH_API_KEY:
|
| 45 |
+
logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.")
|
| 46 |
+
|
| 47 |
# 전역 변수
|
| 48 |
conversation_history = []
|
| 49 |
selected_language = "English" # 기본 언어
|
|
|
|
| 56 |
WRITER_DRAFT_STAGES = [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48] # 작가 초안
|
| 57 |
WRITER_REVISION_STAGES = [5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35, 38, 41, 44, 47, 50] # 작가 수정본
|
| 58 |
|
| 59 |
+
class WebSearchIntegration:
|
| 60 |
+
"""Brave Search API integration for research"""
|
| 61 |
+
|
| 62 |
+
def __init__(self):
|
| 63 |
+
self.brave_api_key = BRAVE_SEARCH_API_KEY
|
| 64 |
+
self.search_url = "https://api.search.brave.com/res/v1/web/search"
|
| 65 |
+
self.enabled = bool(self.brave_api_key)
|
| 66 |
+
|
| 67 |
+
def search(self, query: str, count: int = 5, language: str = "en") -> List[Dict]:
|
| 68 |
+
"""Perform web search using Brave Search API"""
|
| 69 |
+
if not self.enabled:
|
| 70 |
+
return []
|
| 71 |
+
|
| 72 |
+
headers = {
|
| 73 |
+
"Accept": "application/json",
|
| 74 |
+
"X-Subscription-Token": self.brave_api_key
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
# 언어에 따른 검색 설정
|
| 78 |
+
search_lang = "ko" if language == "Korean" else "en"
|
| 79 |
+
|
| 80 |
+
params = {
|
| 81 |
+
"q": query,
|
| 82 |
+
"count": count,
|
| 83 |
+
"search_lang": search_lang,
|
| 84 |
+
"text_decorations": False,
|
| 85 |
+
"safesearch": "moderate"
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
response = requests.get(self.search_url, headers=headers, params=params, timeout=10)
|
| 90 |
+
if response.status_code == 200:
|
| 91 |
+
results = response.json()
|
| 92 |
+
web_results = results.get("web", {}).get("results", [])
|
| 93 |
+
logger.info(f"Search successful: Found {len(web_results)} results for '{query}'")
|
| 94 |
+
return web_results
|
| 95 |
+
else:
|
| 96 |
+
logger.error(f"Search API error: {response.status_code}")
|
| 97 |
+
return []
|
| 98 |
+
except Exception as e:
|
| 99 |
+
logger.error(f"Search error: {str(e)}")
|
| 100 |
+
return []
|
| 101 |
+
|
| 102 |
+
def extract_relevant_info(self, results: List[Dict], max_chars: int = 3000) -> str:
|
| 103 |
+
"""Extract relevant information from search results"""
|
| 104 |
+
if not results:
|
| 105 |
+
return ""
|
| 106 |
+
|
| 107 |
+
extracted = []
|
| 108 |
+
total_chars = 0
|
| 109 |
+
|
| 110 |
+
for i, result in enumerate(results[:5], 1): # 최대 5개 결과
|
| 111 |
+
title = result.get("title", "")
|
| 112 |
+
description = result.get("description", "")
|
| 113 |
+
url = result.get("url", "")
|
| 114 |
+
|
| 115 |
+
# 일부 내용 추출
|
| 116 |
+
extra_text = ""
|
| 117 |
+
if "extra_snippets" in result:
|
| 118 |
+
extra_text = " ".join(result["extra_snippets"][:2])
|
| 119 |
+
|
| 120 |
+
info = f"""[{i}] {title}
|
| 121 |
+
{description}
|
| 122 |
+
{extra_text}
|
| 123 |
+
Source: {url}
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
if total_chars + len(info) < max_chars:
|
| 127 |
+
extracted.append(info)
|
| 128 |
+
total_chars += len(info)
|
| 129 |
+
else:
|
| 130 |
+
break
|
| 131 |
+
|
| 132 |
+
return "\n---\n".join(extracted)
|
| 133 |
+
|
| 134 |
+
def create_research_queries(self, topic: str, role: str, stage_info: Dict, language: str = "English") -> List[str]:
|
| 135 |
+
"""Create multiple research queries based on role and context"""
|
| 136 |
+
queries = []
|
| 137 |
+
|
| 138 |
+
if language == "Korean":
|
| 139 |
+
if role == "director":
|
| 140 |
+
queries = [
|
| 141 |
+
f"{topic} 소설 배경 설정",
|
| 142 |
+
f"{topic} 역사적 사실",
|
| 143 |
+
f"{topic} 문화적 특징"
|
| 144 |
+
]
|
| 145 |
+
elif role.startswith("writer"):
|
| 146 |
+
writer_num = int(role.replace("writer", ""))
|
| 147 |
+
if writer_num <= 3: # 초반부 작가
|
| 148 |
+
queries = [
|
| 149 |
+
f"{topic} 구체적 장면 묘사",
|
| 150 |
+
f"{topic} 전문 용어 설명"
|
| 151 |
+
]
|
| 152 |
+
elif writer_num <= 8: # 중반부 작가
|
| 153 |
+
queries = [
|
| 154 |
+
f"{topic} 갈등 상황 사례",
|
| 155 |
+
f"{topic} 심리적 측면"
|
| 156 |
+
]
|
| 157 |
+
else: # 후반부 작가
|
| 158 |
+
queries = [
|
| 159 |
+
f"{topic} 해결 방법",
|
| 160 |
+
f"{topic} 감동적인 사례"
|
| 161 |
+
]
|
| 162 |
+
elif role == "critic":
|
| 163 |
+
queries = [
|
| 164 |
+
f"{topic} 문학 작품 분석",
|
| 165 |
+
f"{topic} 유사 소설 추천"
|
| 166 |
+
]
|
| 167 |
+
else:
|
| 168 |
+
if role == "director":
|
| 169 |
+
queries = [
|
| 170 |
+
f"{topic} novel setting ideas",
|
| 171 |
+
f"{topic} historical facts",
|
| 172 |
+
f"{topic} cultural aspects"
|
| 173 |
+
]
|
| 174 |
+
elif role.startswith("writer"):
|
| 175 |
+
writer_num = int(role.replace("writer", ""))
|
| 176 |
+
if writer_num <= 3: # Early writers
|
| 177 |
+
queries = [
|
| 178 |
+
f"{topic} vivid scene descriptions",
|
| 179 |
+
f"{topic} technical terminology explained"
|
| 180 |
+
]
|
| 181 |
+
elif writer_num <= 8: # Middle writers
|
| 182 |
+
queries = [
|
| 183 |
+
f"{topic} conflict scenarios",
|
| 184 |
+
f"{topic} psychological aspects"
|
| 185 |
+
]
|
| 186 |
+
else: # Later writers
|
| 187 |
+
queries = [
|
| 188 |
+
f"{topic} resolution methods",
|
| 189 |
+
f"{topic} emotional stories"
|
| 190 |
+
]
|
| 191 |
+
elif role == "critic":
|
| 192 |
+
queries = [
|
| 193 |
+
f"{topic} literary analysis",
|
| 194 |
+
f"{topic} similar novels recommendations"
|
| 195 |
+
]
|
| 196 |
+
|
| 197 |
+
return queries
|
| 198 |
+
|
| 199 |
class NovelDatabase:
|
| 200 |
+
"""Novel session management database with search history"""
|
| 201 |
|
| 202 |
@staticmethod
|
| 203 |
def init_db():
|
|
|
|
| 239 |
)
|
| 240 |
''')
|
| 241 |
|
| 242 |
+
# Search history table - 검색 이력 저장
|
| 243 |
+
cursor.execute('''
|
| 244 |
+
CREATE TABLE IF NOT EXISTS search_history (
|
| 245 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 246 |
+
session_id TEXT NOT NULL,
|
| 247 |
+
stage_number INTEGER NOT NULL,
|
| 248 |
+
role TEXT NOT NULL,
|
| 249 |
+
query TEXT NOT NULL,
|
| 250 |
+
results TEXT,
|
| 251 |
+
created_at TEXT DEFAULT (datetime('now')),
|
| 252 |
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
|
| 253 |
+
)
|
| 254 |
+
''')
|
| 255 |
+
|
| 256 |
# Create indices
|
| 257 |
cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON stages(session_id)')
|
| 258 |
cursor.execute('CREATE INDEX IF NOT EXISTS idx_stage_number ON stages(stage_number)')
|
| 259 |
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_search_session ON search_history(session_id)')
|
| 260 |
|
| 261 |
conn.commit()
|
| 262 |
|
| 263 |
+
@staticmethod
|
| 264 |
+
def save_search_history(session_id: str, stage_number: int, role: str, query: str, results: str):
|
| 265 |
+
"""Save search history"""
|
| 266 |
+
with sqlite3.connect(DB_PATH) as conn:
|
| 267 |
+
cursor = conn.cursor()
|
| 268 |
+
cursor.execute('''
|
| 269 |
+
INSERT INTO search_history (session_id, stage_number, role, query, results)
|
| 270 |
+
VALUES (?, ?, ?, ?, ?)
|
| 271 |
+
''', (session_id, stage_number, role, query, results))
|
| 272 |
+
conn.commit()
|
| 273 |
+
|
| 274 |
+
# ... (이전의 모든 NovelDatabase 메서드들은 동일)
|
| 275 |
@staticmethod
|
| 276 |
@contextmanager
|
| 277 |
def get_db():
|
|
|
|
| 470 |
self.model_id = MODEL_ID
|
| 471 |
self.test_mode = TEST_MODE or not self.token
|
| 472 |
|
| 473 |
+
# Web search integration
|
| 474 |
+
self.web_search = WebSearchIntegration()
|
| 475 |
+
|
| 476 |
if self.test_mode:
|
| 477 |
logger.warning("Running in test mode - no actual API calls will be made.")
|
| 478 |
else:
|
| 479 |
logger.info("Running in production mode with API calls enabled.")
|
| 480 |
+
|
| 481 |
+
if self.web_search.enabled:
|
| 482 |
+
logger.info("Web search is enabled with Brave Search API.")
|
| 483 |
+
else:
|
| 484 |
+
logger.warning("Web search is disabled. Set BRAVE_SEARCH_API_KEY to enable.")
|
| 485 |
|
| 486 |
# Initialize database
|
| 487 |
NovelDatabase.init_db()
|
|
|
|
| 497 |
"Content-Type": "application/json"
|
| 498 |
}
|
| 499 |
|
| 500 |
+
def enhance_prompt_with_research(self, original_prompt: str, role: str,
|
| 501 |
+
topic: str, stage_info: Dict, language: str = "English") -> str:
|
| 502 |
+
"""Enhance prompt with web search results"""
|
| 503 |
+
if not self.web_search.enabled or self.test_mode:
|
| 504 |
+
return original_prompt
|
| 505 |
+
|
| 506 |
+
# Create research queries
|
| 507 |
+
queries = self.web_search.create_research_queries(topic, role, stage_info, language)
|
| 508 |
+
|
| 509 |
+
all_research = []
|
| 510 |
+
for query in queries[:2]: # 최대 2개 쿼리
|
| 511 |
+
logger.info(f"Searching: {query}")
|
| 512 |
+
results = self.web_search.search(query, count=3, language=language)
|
| 513 |
+
if results:
|
| 514 |
+
research_text = self.web_search.extract_relevant_info(results, max_chars=1500)
|
| 515 |
+
if research_text:
|
| 516 |
+
all_research.append(f"### {query}\n{research_text}")
|
| 517 |
+
|
| 518 |
+
# Save search history
|
| 519 |
+
if self.current_session_id:
|
| 520 |
+
NovelDatabase.save_search_history(
|
| 521 |
+
self.current_session_id,
|
| 522 |
+
stage_info.get('stage_idx', 0),
|
| 523 |
+
role,
|
| 524 |
+
query,
|
| 525 |
+
research_text
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
if not all_research:
|
| 529 |
+
return original_prompt
|
| 530 |
+
|
| 531 |
+
# Add research to prompt
|
| 532 |
+
if language == "Korean":
|
| 533 |
+
research_section = f"""
|
| 534 |
+
## 웹 검색 참고 자료:
|
| 535 |
+
{chr(10).join(all_research)}
|
| 536 |
+
|
| 537 |
+
위의 검색 결과를 참고하여 더욱 사실적이고 구체적인 내용을 작성하세요.
|
| 538 |
+
검색 결과의 정보를 창의적으로 활용하되, 직접 인용은 피하고 소설에 자연스럽게 녹여내세요.
|
| 539 |
+
실제 사실과 창작을 적절히 조화시켜 독자가 몰입할 수 있는 이야기를 만드세요.
|
| 540 |
+
"""
|
| 541 |
+
else:
|
| 542 |
+
research_section = f"""
|
| 543 |
+
## Web Search Reference:
|
| 544 |
+
{chr(10).join(all_research)}
|
| 545 |
+
|
| 546 |
+
Use the above search results to create more realistic and specific content.
|
| 547 |
+
Creatively incorporate the information from search results, but avoid direct quotes and naturally blend them into the novel.
|
| 548 |
+
Balance real facts with creative fiction to create an immersive story for readers.
|
| 549 |
+
"""
|
| 550 |
+
|
| 551 |
+
return original_prompt + "\n\n" + research_section
|
| 552 |
+
|
| 553 |
+
# ... (이전의 모든 프롬프트 생성 메서드들은 동일)
|
| 554 |
def create_director_initial_prompt(self, user_query: str, language: str = "English") -> str:
|
| 555 |
"""Director AI initial prompt - Novel planning for 16 writers"""
|
| 556 |
if language == "Korean":
|
|
|
|
| 1190 |
yield chunk + " "
|
| 1191 |
time.sleep(0.02)
|
| 1192 |
|
| 1193 |
+
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str,
|
| 1194 |
+
language: str = "English", stage_info: Dict = None) -> Generator[str, None, None]:
|
| 1195 |
+
"""Streaming LLM API call with web search enhancement"""
|
| 1196 |
|
| 1197 |
if self.test_mode:
|
| 1198 |
logger.info(f"Test mode streaming - Role: {role}, Language: {language}")
|
|
|
|
| 1200 |
yield from self.simulate_streaming(test_response, role)
|
| 1201 |
return
|
| 1202 |
|
| 1203 |
+
# Extract topic from user query for research
|
| 1204 |
+
topic = ""
|
| 1205 |
+
if stage_info and 'query' in stage_info:
|
| 1206 |
+
topic = stage_info['query']
|
| 1207 |
+
|
| 1208 |
+
# Enhance prompt with web search if available
|
| 1209 |
+
if messages and messages[-1]["role"] == "user" and self.web_search.enabled:
|
| 1210 |
+
enhanced_prompt = self.enhance_prompt_with_research(
|
| 1211 |
+
messages[-1]["content"],
|
| 1212 |
+
role,
|
| 1213 |
+
topic,
|
| 1214 |
+
stage_info or {},
|
| 1215 |
+
language
|
| 1216 |
+
)
|
| 1217 |
+
messages[-1]["content"] = enhanced_prompt
|
| 1218 |
+
|
| 1219 |
# Real API call
|
| 1220 |
try:
|
| 1221 |
system_prompts = self.get_system_prompts(language)
|
|
|
|
| 1368 |
logger.error(f"Error during streaming: {str(e)}")
|
| 1369 |
yield f"❌ Error occurred: {str(e)}"
|
| 1370 |
|
| 1371 |
+
# ... (나머지 메서드들은 동일)
|
| 1372 |
def get_system_prompts(self, language: str) -> Dict[str, str]:
|
| 1373 |
"""Get system prompts for all 16 writers with enhanced length requirements"""
|
| 1374 |
if language == "Korean":
|
|
|
|
| 1591 |
def process_novel_stream(self, query: str, language: str = "English",
|
| 1592 |
session_id: Optional[str] = None,
|
| 1593 |
resume_from_stage: int = 0) -> Generator[Tuple[str, List[Dict[str, str]]], None, None]:
|
| 1594 |
+
"""Process novel writing with streaming updates and web search"""
|
| 1595 |
try:
|
| 1596 |
global conversation_history
|
| 1597 |
|
|
|
|
| 1654 |
for stage_idx in range(resume_from_stage, len(stage_definitions)):
|
| 1655 |
role, stage_name = stage_definitions[stage_idx]
|
| 1656 |
|
| 1657 |
+
# Add search indicator if enabled
|
| 1658 |
+
if self.web_search.enabled and not self.test_mode:
|
| 1659 |
+
stage_name += " 🔍"
|
| 1660 |
+
|
| 1661 |
# Add stage if not already present
|
| 1662 |
if stage_idx >= len(stages):
|
| 1663 |
stages.append({
|
|
|
|
| 1673 |
# Get appropriate prompt based on stage
|
| 1674 |
prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
|
| 1675 |
|
| 1676 |
+
# Create stage info for web search
|
| 1677 |
+
stage_info = {
|
| 1678 |
+
'stage_idx': stage_idx,
|
| 1679 |
+
'query': query,
|
| 1680 |
+
'stage_name': stage_name
|
| 1681 |
+
}
|
| 1682 |
+
|
| 1683 |
stage_content = ""
|
| 1684 |
|
| 1685 |
+
# Stream content generation with web search
|
| 1686 |
for chunk in self.call_llm_streaming(
|
| 1687 |
[{"role": "user", "content": prompt}],
|
| 1688 |
role,
|
| 1689 |
+
language,
|
| 1690 |
+
stage_info
|
| 1691 |
):
|
| 1692 |
stage_content += chunk
|
| 1693 |
stages[stage_idx]["content"] = stage_content
|
|
|
|
| 1822 |
|
| 1823 |
return ""
|
| 1824 |
|
| 1825 |
+
# Gradio Interface Functions (동일)
|
| 1826 |
def process_query(query: str, language: str, session_id: str = None) -> Generator[Tuple[str, str, str], None, None]:
|
| 1827 |
"""Process query and yield updates"""
|
| 1828 |
if not query.strip() and not session_id:
|
|
|
|
| 2028 |
border-radius: 8px;
|
| 2029 |
margin-top: 20px;
|
| 2030 |
}
|
| 2031 |
+
|
| 2032 |
+
.search-indicator {
|
| 2033 |
+
color: #4CAF50;
|
| 2034 |
+
font-weight: bold;
|
| 2035 |
+
}
|
| 2036 |
"""
|
| 2037 |
|
| 2038 |
# Create Gradio Interface
|
|
|
|
| 2044 |
📚 SOMA Novel Writing System
|
| 2045 |
</h1>
|
| 2046 |
<h3 style="color: #ccc; margin-bottom: 20px;">
|
| 2047 |
+
AI Collaborative Novel Generation with Web Research - 48 Page Novella Creator
|
| 2048 |
</h3>
|
| 2049 |
<p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
|
| 2050 |
Enter a theme or prompt, and watch as 19 AI agents collaborate to create a complete 48-page novella.
|
| 2051 |
The system includes 1 Director, 1 Critic, and 16 Writers (each writing 3 pages) working in harmony.
|
| 2052 |
+
<span class="search-indicator">🔍 Web search enabled</span> - agents will research relevant information for more realistic content.
|
| 2053 |
All progress is automatically saved and can be resumed anytime.
|
| 2054 |
</p>
|
| 2055 |
</div>
|
|
|
|
| 2073 |
label="Language / 언어"
|
| 2074 |
)
|
| 2075 |
|
| 2076 |
+
# Web search status indicator
|
| 2077 |
+
web_search_status = gr.Markdown(
|
| 2078 |
+
value=f"🔍 **Web Search:** {'Enabled' if WebSearchIntegration().enabled else 'Disabled (Set BRAVE_SEARCH_API_KEY)'}"
|
| 2079 |
+
)
|
| 2080 |
+
|
| 2081 |
with gr.Row():
|
| 2082 |
submit_btn = gr.Button("🚀 Start Writing / 작성 시작", variant="primary", scale=2)
|
| 2083 |
clear_btn = gr.Button("🗑️ Clear / 초기화", scale=1)
|
|
|
|
| 2253 |
else:
|
| 2254 |
logger.info(f"Running in PRODUCTION MODE with API endpoint: {API_URL}")
|
| 2255 |
|
| 2256 |
+
# Check web search
|
| 2257 |
+
if BRAVE_SEARCH_API_KEY:
|
| 2258 |
+
logger.info("Web search is ENABLED with Brave Search API")
|
| 2259 |
+
else:
|
| 2260 |
+
logger.warning("Web search is DISABLED. Set BRAVE_SEARCH_API_KEY environment variable to enable.")
|
| 2261 |
+
|
| 2262 |
# Initialize database on startup
|
| 2263 |
logger.info("Initializing database...")
|
| 2264 |
NovelDatabase.init_db()
|