Coverage for src / local_deep_research / followup_research / routes.py: 11%

92 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-01-11 00:51 +0000

1""" 

2Flask routes for follow-up research functionality. 

3""" 

4 

5from flask import Blueprint, request, jsonify, session, g 

6from loguru import logger 

7 

8from .service import FollowUpResearchService 

9from .models import FollowUpRequest 

10from ..web.auth.decorators import login_required 

11 

12# Create blueprint 

13followup_bp = Blueprint("followup", __name__, url_prefix="/api/followup") 

14 

15 

16@followup_bp.route("/prepare", methods=["POST"]) 

17@login_required 

18def prepare_followup(): 

19 """ 

20 Prepare a follow-up research by loading parent context. 

21 

22 Request body: 

23 { 

24 "parent_research_id": "uuid", 

25 "question": "follow-up question" 

26 } 

27 

28 Returns: 

29 { 

30 "success": true, 

31 "parent_summary": "...", 

32 "available_sources": 10, 

33 "suggested_strategy": "source-based" 

34 } 

35 """ 

36 try: 

37 data = request.get_json() 

38 parent_id = data.get("parent_research_id") 

39 question = data.get("question") 

40 

41 if not parent_id or not question: 

42 return jsonify( 

43 { 

44 "success": False, 

45 "error": "Missing parent_research_id or question", 

46 } 

47 ), 400 

48 

49 # Get username from session 

50 username = session.get("username") 

51 

52 # Get settings snapshot to use for suggested strategy 

53 from ..web.services.settings_manager import SettingsManager 

54 from ..database.session_context import get_user_db_session 

55 

56 with get_user_db_session(username) as db_session: 

57 settings_manager = SettingsManager(db_session=db_session) 

58 settings_snapshot = settings_manager.get_all_settings() 

59 

60 # Get strategy from settings 

61 strategy_from_settings = settings_snapshot.get( 

62 "search.search_strategy", {} 

63 ).get("value", "source-based") 

64 

65 # Initialize service 

66 service = FollowUpResearchService(username=username) 

67 

68 # Load parent context 

69 parent_data = service.load_parent_research(parent_id) 

70 

71 if not parent_data: 

72 # For now, return success with empty data to allow testing 

73 logger.warning( 

74 f"Parent research {parent_id} not found, returning empty context" 

75 ) 

76 return jsonify( 

77 { 

78 "success": True, 

79 "parent_summary": "Previous research context", 

80 "available_sources": 0, 

81 "suggested_strategy": strategy_from_settings, # Use strategy from settings 

82 "parent_research": { 

83 "id": parent_id, 

84 "query": "Previous query", 

85 "sources_count": 0, 

86 }, 

87 } 

88 ) 

89 

90 # Prepare response with parent context summary 

91 response = { 

92 "success": True, 

93 "parent_summary": parent_data.get("query", ""), 

94 "available_sources": len(parent_data.get("resources", [])), 

95 "suggested_strategy": strategy_from_settings, # Use strategy from settings 

96 "parent_research": { 

97 "id": parent_id, 

98 "query": parent_data.get("query", ""), 

99 "sources_count": len(parent_data.get("resources", [])), 

100 }, 

101 } 

102 

103 return jsonify(response) 

104 

105 except Exception: 

106 logger.exception("Error preparing follow-up") 

107 return jsonify( 

108 {"success": False, "error": "An internal error has occurred."} 

109 ), 500 

110 

111 

112@followup_bp.route("/start", methods=["POST"]) 

113@login_required 

114def start_followup(): 

115 """ 

116 Start a follow-up research. 

117 

118 Request body: 

119 { 

120 "parent_research_id": "uuid", 

121 "question": "follow-up question", 

122 "strategy": "source-based", # optional 

123 "max_iterations": 1, # optional 

124 "questions_per_iteration": 3 # optional 

125 } 

126 

127 Returns: 

128 { 

129 "success": true, 

130 "research_id": "new-uuid", 

131 "message": "Follow-up research started" 

132 } 

133 """ 

134 try: 

135 from ..web.services.research_service import ( 

136 start_research_process, 

137 run_research_process, 

138 ) 

139 from ..web.routes.globals import active_research, termination_flags 

140 import uuid 

141 

142 data = request.get_json() 

143 

144 # Get username from session 

145 username = session.get("username") 

146 

147 # Get settings snapshot first to use database values 

148 from ..web.services.settings_manager import SettingsManager 

149 from ..database.session_context import get_user_db_session 

150 

151 with get_user_db_session(username) as db_session: 

152 settings_manager = SettingsManager(db_session=db_session) 

153 settings_snapshot = settings_manager.get_all_settings() 

154 

155 # Get strategy from settings snapshot, fallback to source-based if not set 

156 strategy_from_settings = settings_snapshot.get( 

157 "search.search_strategy", {} 

158 ).get("value", "source-based") 

159 

160 # Get iterations and questions from settings snapshot 

161 iterations_from_settings = settings_snapshot.get( 

162 "search.iterations", {} 

163 ).get("value", 1) 

164 questions_from_settings = settings_snapshot.get( 

165 "search.questions_per_iteration", {} 

166 ).get("value", 3) 

167 

168 # Create follow-up request using settings values 

169 followup_request = FollowUpRequest( 

170 parent_research_id=data.get("parent_research_id"), 

171 question=data.get("question"), 

172 strategy=strategy_from_settings, # Use strategy from settings 

173 max_iterations=iterations_from_settings, # Use iterations from settings 

174 questions_per_iteration=questions_from_settings, # Use questions from settings 

175 ) 

176 

177 # Initialize service 

178 service = FollowUpResearchService(username=username) 

179 

180 # Prepare research parameters 

181 research_params = service.perform_followup(followup_request) 

182 

183 logger.info(f"Research params type: {type(research_params)}") 

184 logger.info( 

185 f"Research params keys: {research_params.keys() if isinstance(research_params, dict) else 'Not a dict'}" 

186 ) 

187 logger.info( 

188 f"Query value: {research_params.get('query') if isinstance(research_params, dict) else 'N/A'}" 

189 ) 

190 logger.info( 

191 f"Query type: {type(research_params.get('query')) if isinstance(research_params, dict) else 'N/A'}" 

192 ) 

193 

194 # Generate new research ID 

195 research_id = str(uuid.uuid4()) 

196 

197 # Create database entry (settings_snapshot already captured above) 

198 from ..database.models import ResearchHistory 

199 from datetime import datetime, UTC 

200 

201 created_at = datetime.now(UTC).isoformat() 

202 

203 with get_user_db_session(username) as db_session: 

204 # Create the database entry (required for tracking) 

205 research_meta = { 

206 "submission": { 

207 "parent_research_id": data.get("parent_research_id"), 

208 "question": data.get("question"), 

209 "strategy": "contextual-followup", 

210 }, 

211 } 

212 

213 research = ResearchHistory( 

214 id=research_id, 

215 query=research_params["query"], 

216 mode="quick", # Use 'quick' not 'quick_summary' 

217 status="in_progress", 

218 created_at=created_at, 

219 progress_log=[{"time": created_at, "progress": 0}], 

220 research_meta=research_meta, 

221 ) 

222 db_session.add(research) 

223 db_session.commit() 

224 logger.info( 

225 f"Created follow-up research entry with ID: {research_id}" 

226 ) 

227 

228 # Start the research process using the existing infrastructure 

229 # Use quick_summary mode for follow-ups by default 

230 logger.info( 

231 f"Starting follow-up research for query of type: {type(research_params.get('query'))}" 

232 ) 

233 

234 # Get user password for metrics database access 

235 user_password = None 

236 session_id = session.get("session_id") 

237 if session_id: 

238 from ..database.session_passwords import session_password_store 

239 

240 user_password = session_password_store.retrieve( 

241 username, session_id 

242 ) 

243 

244 # Fallback to g.user_password (set by middleware if temp_auth was used) 

245 if not user_password: 

246 user_password = getattr(g, "user_password", None) 

247 

248 # Last resort: try temp_auth_store 

249 if not user_password: 

250 from ..database.temp_auth import temp_auth_store 

251 

252 auth_token = session.get("temp_auth_token") 

253 if auth_token: 

254 # Use peek_auth to avoid consuming the token 

255 auth_data = temp_auth_store.peek_auth(auth_token) 

256 if auth_data and auth_data[0] == username: 

257 user_password = auth_data[1] 

258 

259 if not user_password: 

260 logger.warning( 

261 f"No password available for metrics access for user {username}" 

262 ) 

263 

264 # Get model and search settings from user's settings 

265 model_provider = settings_snapshot.get("llm.provider", {}).get( 

266 "value", "OLLAMA" 

267 ) 

268 model = settings_snapshot.get("llm.model", {}).get( 

269 "value", "gemma3:12b" 

270 ) 

271 search_engine = settings_snapshot.get("search.tool", {}).get( 

272 "value", "searxng" 

273 ) 

274 custom_endpoint = settings_snapshot.get( 

275 "llm.openai_endpoint.url", {} 

276 ).get("value") 

277 

278 start_research_process( 

279 research_id, 

280 research_params["query"], 

281 "quick", # Use 'quick' for quick summary mode 

282 active_research, 

283 termination_flags, 

284 run_research_process, 

285 username=username, 

286 user_password=user_password, # Pass password for metrics database access 

287 model_provider=model_provider, # Pass model provider 

288 model=model, # Pass model name 

289 search_engine=search_engine, # Pass search engine 

290 custom_endpoint=custom_endpoint, # Pass custom endpoint if any 

291 strategy="enhanced-contextual-followup", # Use enhanced contextual follow-up strategy 

292 iterations=research_params["max_iterations"], 

293 questions_per_iteration=research_params["questions_per_iteration"], 

294 delegate_strategy=research_params.get( 

295 "delegate_strategy", "source-based" 

296 ), 

297 research_context=research_params["research_context"], 

298 parent_research_id=research_params[ 

299 "parent_research_id" 

300 ], # Pass parent research ID 

301 settings_snapshot=settings_snapshot, 

302 ) 

303 

304 return jsonify( 

305 { 

306 "success": True, 

307 "research_id": research_id, 

308 "message": "Follow-up research started", 

309 } 

310 ) 

311 

312 except Exception: 

313 logger.exception("Error starting follow-up") 

314 return jsonify( 

315 {"success": False, "error": "An internal error has occurred."} 

316 ), 500