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

93 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-25 01:07 +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 ..constants import ResearchStatus 

9from .service import FollowUpResearchService 

10from .models import FollowUpRequest 

11from ..web.auth.decorators import login_required 

12 

13# Create blueprint 

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

15 

16 

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

18@login_required 

19def prepare_followup(): 

20 """ 

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

22 

23 Request body: 

24 { 

25 "parent_research_id": "uuid", 

26 "question": "follow-up question" 

27 } 

28 

29 Returns: 

30 { 

31 "success": true, 

32 "parent_summary": "...", 

33 "available_sources": 10, 

34 "suggested_strategy": "source-based" 

35 } 

36 """ 

37 try: 

38 data = request.get_json() 

39 parent_id = data.get("parent_research_id") 

40 question = data.get("question") 

41 

42 if not parent_id or not question: 

43 return jsonify( 

44 { 

45 "success": False, 

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

47 } 

48 ), 400 

49 

50 # Get username from session 

51 username = session.get("username") 

52 

53 # Get settings snapshot to use for suggested strategy 

54 from ..settings.manager import SettingsManager 

55 from ..database.session_context import get_user_db_session 

56 

57 with get_user_db_session(username) as db_session: 

58 settings_manager = SettingsManager(db_session=db_session) 

59 settings_snapshot = settings_manager.get_all_settings() 

60 

61 # Get strategy from settings 

62 strategy_from_settings = settings_snapshot.get( 

63 "search.search_strategy", {} 

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

65 

66 # Initialize service 

67 service = FollowUpResearchService(username=username) 

68 

69 # Load parent context 

70 parent_data = service.load_parent_research(parent_id) 

71 

72 if not parent_data: 

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

74 logger.warning( 

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

76 ) 

77 return jsonify( 

78 { 

79 "success": True, 

80 "parent_summary": "Previous research context", 

81 "available_sources": 0, 

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

83 "parent_research": { 

84 "id": parent_id, 

85 "query": "Previous query", 

86 "sources_count": 0, 

87 }, 

88 } 

89 ) 

90 

91 # Prepare response with parent context summary 

92 response = { 

93 "success": True, 

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

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

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

97 "parent_research": { 

98 "id": parent_id, 

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

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

101 }, 

102 } 

103 

104 return jsonify(response) 

105 

106 except Exception: 

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

108 return jsonify( 

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

110 ), 500 

111 

112 

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

114@login_required 

115def start_followup(): 

116 """ 

117 Start a follow-up research. 

118 

119 Request body: 

120 { 

121 "parent_research_id": "uuid", 

122 "question": "follow-up question", 

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

124 "max_iterations": 1, # optional 

125 "questions_per_iteration": 3 # optional 

126 } 

127 

128 Returns: 

129 { 

130 "success": true, 

131 "research_id": "new-uuid", 

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

133 } 

134 """ 

135 try: 

136 from ..web.services.research_service import ( 

137 start_research_process, 

138 run_research_process, 

139 ) 

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

141 import uuid 

142 

143 data = request.get_json() 

144 

145 # Get username from session 

146 username = session.get("username") 

147 

148 # Get settings snapshot first to use database values 

149 from ..settings.manager import SettingsManager 

150 from ..database.session_context import get_user_db_session 

151 

152 with get_user_db_session(username) as db_session: 

153 settings_manager = SettingsManager(db_session=db_session) 

154 settings_snapshot = settings_manager.get_all_settings() 

155 

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

157 strategy_from_settings = settings_snapshot.get( 

158 "search.search_strategy", {} 

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

160 

161 # Get iterations and questions from settings snapshot 

162 iterations_from_settings = settings_snapshot.get( 

163 "search.iterations", {} 

164 ).get("value", 1) 

165 questions_from_settings = settings_snapshot.get( 

166 "search.questions_per_iteration", {} 

167 ).get("value", 3) 

168 

169 # Create follow-up request using settings values 

170 followup_request = FollowUpRequest( 

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

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

173 strategy=strategy_from_settings, # Use strategy from settings 

174 max_iterations=iterations_from_settings, # Use iterations from settings 

175 questions_per_iteration=questions_from_settings, # Use questions from settings 

176 ) 

177 

178 # Initialize service 

179 service = FollowUpResearchService(username=username) 

180 

181 # Prepare research parameters 

182 research_params = service.perform_followup(followup_request) 

183 

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

185 logger.info( 

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

187 ) 

188 logger.info( 

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

190 ) 

191 logger.info( 

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

193 ) 

194 

195 # Generate new research ID 

196 research_id = str(uuid.uuid4()) 

197 

198 # Create database entry (settings_snapshot already captured above) 

199 from ..database.models import ResearchHistory 

200 from datetime import datetime, UTC 

201 

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

203 

204 with get_user_db_session(username) as db_session: 

205 # Create the database entry (required for tracking) 

206 research_meta = { 

207 "submission": { 

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

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

210 "strategy": "contextual-followup", 

211 }, 

212 } 

213 

214 research = ResearchHistory( 

215 id=research_id, 

216 query=research_params["query"], 

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

218 status=ResearchStatus.IN_PROGRESS, 

219 created_at=created_at, 

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

221 research_meta=research_meta, 

222 ) 

223 db_session.add(research) 

224 db_session.commit() 

225 logger.info( 

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

227 ) 

228 

229 # Start the research process using the existing infrastructure 

230 # Use quick_summary mode for follow-ups by default 

231 logger.info( 

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

233 ) 

234 

235 # Get user password for metrics database access 

236 user_password = None 

237 session_id = session.get("session_id") 

238 if session_id: 238 ↛ 239line 238 didn't jump to line 239 because the condition on line 238 was never true

239 from ..database.session_passwords import session_password_store 

240 

241 user_password = session_password_store.retrieve( 

242 username, session_id 

243 ) 

244 

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

246 if not user_password: 246 ↛ 250line 246 didn't jump to line 250 because the condition on line 246 was always true

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

248 

249 # Last resort: try temp_auth_store 

250 if not user_password: 250 ↛ 260line 250 didn't jump to line 260 because the condition on line 250 was always true

251 from ..database.temp_auth import temp_auth_store 

252 

253 auth_token = session.get("temp_auth_token") 

254 if auth_token: 254 ↛ 256line 254 didn't jump to line 256 because the condition on line 254 was never true

255 # Use peek_auth to avoid consuming the token 

256 auth_data = temp_auth_store.peek_auth(auth_token) 

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

258 user_password = auth_data[1] 

259 

260 if not user_password: 260 ↛ 266line 260 didn't jump to line 266 because the condition on line 260 was always true

261 logger.warning( 

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

263 ) 

264 

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

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

267 "value", "OLLAMA" 

268 ) 

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

270 "value", "gemma3:12b" 

271 ) 

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

273 "value", "searxng" 

274 ) 

275 custom_endpoint = settings_snapshot.get( 

276 "llm.openai_endpoint.url", {} 

277 ).get("value") 

278 

279 start_research_process( 

280 research_id, 

281 research_params["query"], 

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

283 active_research, 

284 termination_flags, 

285 run_research_process, 

286 username=username, 

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

288 model_provider=model_provider, # Pass model provider 

289 model=model, # Pass model name 

290 search_engine=search_engine, # Pass search engine 

291 custom_endpoint=custom_endpoint, # Pass custom endpoint if any 

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

293 iterations=research_params["max_iterations"], 

294 questions_per_iteration=research_params["questions_per_iteration"], 

295 delegate_strategy=research_params.get( 

296 "delegate_strategy", "source-based" 

297 ), 

298 research_context=research_params["research_context"], 

299 parent_research_id=research_params[ 

300 "parent_research_id" 

301 ], # Pass parent research ID 

302 settings_snapshot=settings_snapshot, 

303 ) 

304 

305 return jsonify( 

306 { 

307 "success": True, 

308 "research_id": research_id, 

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

310 } 

311 ) 

312 

313 except Exception: 

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

315 return jsonify( 

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

317 ), 500