openfree commited on
Commit
a81bc54
·
verified ·
1 Parent(s): 06baaa1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +288 -8
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, language: str = "English") -> Generator[str, None, None]:
961
- """Streaming LLM API call with improved prompting"""
 
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()