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
« 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"""
5from flask import Blueprint, request, jsonify, session, g
6from loguru import logger
8from .service import FollowUpResearchService
9from .models import FollowUpRequest
10from ..web.auth.decorators import login_required
12# Create blueprint
13followup_bp = Blueprint("followup", __name__, url_prefix="/api/followup")
16@followup_bp.route("/prepare", methods=["POST"])
17@login_required
18def prepare_followup():
19 """
20 Prepare a follow-up research by loading parent context.
22 Request body:
23 {
24 "parent_research_id": "uuid",
25 "question": "follow-up question"
26 }
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")
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
49 # Get username from session
50 username = session.get("username")
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
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()
60 # Get strategy from settings
61 strategy_from_settings = settings_snapshot.get(
62 "search.search_strategy", {}
63 ).get("value", "source-based")
65 # Initialize service
66 service = FollowUpResearchService(username=username)
68 # Load parent context
69 parent_data = service.load_parent_research(parent_id)
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 )
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 }
103 return jsonify(response)
105 except Exception:
106 logger.exception("Error preparing follow-up")
107 return jsonify(
108 {"success": False, "error": "An internal error has occurred."}
109 ), 500
112@followup_bp.route("/start", methods=["POST"])
113@login_required
114def start_followup():
115 """
116 Start a follow-up research.
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 }
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
142 data = request.get_json()
144 # Get username from session
145 username = session.get("username")
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
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()
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")
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)
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 )
177 # Initialize service
178 service = FollowUpResearchService(username=username)
180 # Prepare research parameters
181 research_params = service.perform_followup(followup_request)
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 )
194 # Generate new research ID
195 research_id = str(uuid.uuid4())
197 # Create database entry (settings_snapshot already captured above)
198 from ..database.models import ResearchHistory
199 from datetime import datetime, UTC
201 created_at = datetime.now(UTC).isoformat()
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 }
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 )
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 )
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
240 user_password = session_password_store.retrieve(
241 username, session_id
242 )
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)
248 # Last resort: try temp_auth_store
249 if not user_password:
250 from ..database.temp_auth import temp_auth_store
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]
259 if not user_password:
260 logger.warning(
261 f"No password available for metrics access for user {username}"
262 )
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")
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 )
304 return jsonify(
305 {
306 "success": True,
307 "research_id": research_id,
308 "message": "Follow-up research started",
309 }
310 )
312 except Exception:
313 logger.exception("Error starting follow-up")
314 return jsonify(
315 {"success": False, "error": "An internal error has occurred."}
316 ), 500