Skip to content

Commit 076b159

Browse files
committed
feat: Enhance form submission and filling with robust dropdown handling and detailed logging
- Implemented a robust dropdown filling method with multiple selection strategies and logging. - Improved field filling logic to handle custom dropdowns and added detailed logging for each step. - Enhanced form submission method with prioritized selectors and logging for success indicators. - Added validation for form schemas before caching to prevent stale data. - Introduced a new entry point script (run.py) to handle event loop policy on Windows for Uvicorn. - Created a server entry point (server.py) to ensure proper event loop handling for Playwright. - Updated frontend VoiceFormFiller component to handle cases with no detectable fields.
1 parent 70470ce commit 076b159

20 files changed

Lines changed: 724 additions & 203 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ debug_*.png
6969
*.log
7070
temp/
7171
tmp/
72+
form-flow-backend/storage/
7273

7374
# Testing
7475
.pytest_cache/

form-flow-backend/main.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@
1818
"""
1919

2020

21-
import warnings
2221
import sys
22+
import os
23+
import warnings
2324
import asyncio
25+
from concurrent.futures import ThreadPoolExecutor
26+
import time
27+
import shutil
2428

2529
# Fix for Playwright on Windows - ProactorEventLoop required for subprocess
2630
if sys.platform == 'win32':
@@ -59,12 +63,42 @@ async def lifespan(app: FastAPI):
5963
"""
6064
Application lifespan context manager.
6165
62-
Handles startup and shutdown events:
63-
- Startup: Initialize database tables
66+
Handlers startup and shutdown events:
67+
- Startup: Initialize database tables, setup thread pool, and start cleanup tasks
6468
- Shutdown: Cleanup resources
6569
"""
6670
# Startup
6771
logger.info(f"Starting {settings.APP_NAME} v{settings.APP_VERSION}")
72+
73+
# Initialize ThreadPoolExecutor for background AI inference (max 2 workers)
74+
app.state.thread_pool = ThreadPoolExecutor(max_workers=2)
75+
logger.info("Initialized AI ThreadPoolExecutor (max_workers=2)")
76+
77+
# Start periodic cleanup of temp files
78+
async def cleanup_temp_files():
79+
temp_dir = "/tmp/formflow"
80+
if not os.path.exists(temp_dir):
81+
return
82+
83+
while True:
84+
try:
85+
now = time.time()
86+
for filename in os.listdir(temp_dir):
87+
filepath = os.path.join(temp_dir, filename)
88+
# Delete if older than 1 hour
89+
if os.path.getmtime(filepath) < now - 3600:
90+
if os.path.isfile(filepath):
91+
os.remove(filepath)
92+
elif os.path.isdir(filepath):
93+
shutil.rmtree(filepath)
94+
logger.debug("Cleaned up old temp files")
95+
except Exception as e:
96+
logger.error(f"Temp file cleanup failed: {e}")
97+
98+
await asyncio.sleep(1800) # Run every 30 minutes
99+
100+
asyncio.create_task(cleanup_temp_files())
101+
logger.info("Started periodic temp file cleanup task")
68102
logger.info(f"Debug mode: {settings.DEBUG}")
69103

70104
# Create database tables
@@ -96,6 +130,11 @@ async def lifespan(app: FastAPI):
96130
# Shutdown
97131
logger.info("Shutting down application")
98132

133+
# Shutdown thread pool
134+
if hasattr(app.state, 'thread_pool'):
135+
app.state.thread_pool.shutdown(wait=True)
136+
logger.info("AI ThreadPoolExecutor shut down")
137+
99138
# Close browser pool to prevent zombie processes
100139
from services.form.browser_pool import close_browser_pool
101140
await close_browser_pool()

form-flow-backend/routers/attachments.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,43 @@ async def upload_attachment(
178178
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
179179

180180

181+
@router.post("/upload-temp")
182+
async def upload_temp_file(file: UploadFile = File(...)):
183+
"""
184+
Temporary file upload for form filling.
185+
Saves to /tmp/formflow and returns absolute path.
186+
"""
187+
try:
188+
if not file:
189+
raise HTTPException(status_code=400, detail="No file provided")
190+
191+
temp_dir = "/tmp/formflow"
192+
os.makedirs(temp_dir, exist_ok=True)
193+
194+
# Use filename with uuid prefix to avoid collisions
195+
temp_filename = f"{uuid.uuid4()}_{file.filename}"
196+
temp_path = os.path.join(temp_dir, temp_filename)
197+
198+
# Ensure path is absolute as requested
199+
abs_temp_path = os.path.abspath(temp_path)
200+
201+
with open(abs_temp_path, "wb") as f:
202+
content = await file.read()
203+
f.write(content)
204+
205+
logger.info(f"📁 Temp file uploaded to: {abs_temp_path}")
206+
207+
return {
208+
"success": True,
209+
"temp_path": abs_temp_path,
210+
"filename": file.filename,
211+
"message": "Temporary file uploaded successfully"
212+
}
213+
except Exception as e:
214+
logger.error(f"Temp upload failed: {e}")
215+
raise HTTPException(status_code=500, detail=f"Temp upload failed: {str(e)}")
216+
217+
181218
@router.get("/{file_id}")
182219
async def get_attachment(file_id: str):
183220
"""

form-flow-backend/routers/conversation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class CreateSessionRequest(BaseModel):
3333
"""Request to create a new conversation session."""
3434
form_schema: List[Dict[str, Any]]
3535
form_url: str = ""
36-
initial_data: Optional[Dict[str, str]] = None
36+
initial_data: Optional[Dict[str, Any]] = None
3737
client_type: Optional[str] = "extension" # Defaults to 'extension' for backward compat
3838

3939

@@ -54,7 +54,7 @@ class MessageRequest(BaseModel):
5454
class MessageResponse(BaseModel):
5555
"""Response after processing a message."""
5656
response: str
57-
extracted_values: Dict[str, str]
57+
extracted_values: Dict[str, Any]
5858
confidence_scores: Dict[str, float]
5959
needs_confirmation: List[str]
6060
remaining_fields_count: int
@@ -73,7 +73,7 @@ class SessionSummary(BaseModel):
7373
"""Summary of a conversation session."""
7474
session_id: str
7575
form_url: str
76-
extracted_fields: Dict[str, str]
76+
extracted_fields: Dict[str, Any]
7777
remaining_count: int
7878
is_complete: bool
7979
conversation_turns: int

form-flow-backend/routers/forms.py

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
from fastapi import APIRouter, HTTPException, Depends, Request, BackgroundTasks
22
from sqlalchemy.ext.asyncio import AsyncSession
3-
from typing import Dict, List, Any
3+
from sqlalchemy.future import select
4+
from core import database, models
5+
import auth
6+
from config.settings import settings
7+
from typing import Dict, List, Any, Optional
48
from pydantic import BaseModel
59
import asyncio
610
import json
11+
import hashlib
712

813
from services.form.parser import get_form_schema, create_template
914
from core.dependencies import (
@@ -16,11 +21,12 @@
1621
from services.ai.gemini import GeminiService, SmartFormFillerChain
1722
from services.form.conventions import get_form_schema as get_schema
1823
from services.ai.smart_autofill import get_smart_autofill
19-
from core import database, models
20-
import auth
21-
from config.settings import settings
22-
from sqlalchemy.future import select
2324
from services.ai.profile.service import generate_profile_background
25+
from utils.cache import get_cached, set_cached
26+
from utils.ws import emit_field_filled
27+
from routers.websocket import manager
28+
from utils.cache import get_cached, set_cached
29+
import hashlib
2430

2531
# --- Pydantic Models ---
2632
class ScrapeRequest(BaseModel):
@@ -33,16 +39,16 @@ class VoiceProcessRequest(BaseModel):
3339

3440
class FormFillRequest(BaseModel):
3541
url: str
36-
form_data: Dict[str, str]
42+
form_data: Dict[str, Any]
3743

3844
class FormSubmitRequest(BaseModel):
3945
url: str
40-
form_data: Dict[str, str]
46+
form_data: Dict[str, Any]
4147
form_schema: List[Dict[str, Any]]
4248
use_cdp: bool = False # If True, connect to user's browser via Chrome DevTools Protocol
4349

4450
class ConversationalFlowRequest(BaseModel):
45-
extracted_fields: Dict[str, str]
51+
extracted_fields: Dict[str, Any]
4652
form_schema: List[Dict[str, Any]]
4753

4854
class ComprehensiveFormRequest(BaseModel):
@@ -52,6 +58,8 @@ class ComprehensiveFormRequest(BaseModel):
5258
class MagicFillRequest(BaseModel):
5359
form_schema: List[Dict[str, Any]]
5460
user_profile: Dict[str, Any] = {} # Optional extra profile data
61+
form_url: Optional[str] = None
62+
form_url: str = "" # Required for caching
5563

5664
router = APIRouter(tags=["Forms & Automation"])
5765

@@ -273,7 +281,7 @@ async def process_voice(
273281
"""Process voice input with LLM enhancement and smart formatting."""
274282
try:
275283
# Process with LLM
276-
result = voice_processor.process_voice_input(
284+
result = await voice_processor.process_voice_input(
277285
data.transcript,
278286
data.field_info,
279287
data.form_context
@@ -399,30 +407,32 @@ async def fill_form(data: FormFillRequest):
399407
async def magic_fill(
400408
data: MagicFillRequest,
401409
request: Request,
410+
background_tasks: BackgroundTasks,
402411
db: AsyncSession = Depends(database.get_db),
403412
gemini_service: GeminiService = Depends(get_gemini_service)
404413
):
405414
"""
406-
🪄 Magic Fill - Intelligently pre-fill entire form from user profile.
407-
408-
Uses LangChain + Gemini to map user data to form fields.
415+
Magic Fill - Intelligently pre-fill entire form from user profile.
416+
Order: instant fill -> parallel AI -> stream progress via WebSocket.
409417
"""
410418
try:
411-
# 1. Get user profile from database if authenticated
412-
user_profile = dict(data.user_profile) # Start with request data
413-
419+
client_id = request.headers.get("X-Client-ID") or request.query_params.get("client_id")
420+
cache_user_id = "anonymous"
421+
user_profile = dict(data.user_profile)
422+
423+
# Merge DB profile + history when authenticated
414424
auth_header = request.headers.get('Authorization')
415425
if auth_header and auth_header.startswith('Bearer '):
416-
token = auth_header.split(' ')[1]
417426
try:
427+
token = auth_header.split(' ')[1]
418428
payload = auth.decode_access_token(token)
419429
if payload:
420430
email = payload.get("sub")
421431
if email:
422432
result = await db.execute(select(models.User).filter(models.User.email == email))
423433
user = result.scalars().first()
424434
if user:
425-
# Merge DB profile with request profile (request takes precedence)
435+
cache_user_id = str(user.id)
426436
db_profile = {
427437
"first_name": user.first_name,
428438
"last_name": user.last_name,
@@ -434,23 +444,28 @@ async def magic_fill(
434444
"fullname": f"{user.first_name} {user.last_name}".strip()
435445
}
436446
user_profile = {**db_profile, **user_profile}
437-
438-
# 🧠 Merge with learned history (fills gaps like Company, Title, specific addresses)
439447
try:
440448
history_profile = await get_smart_autofill().get_profile_from_history(str(user.id))
441449
if history_profile:
442-
print(f"🧠 Merging {len(history_profile)} learned fields from history")
443-
# Base = History, Overlay = Current Result (DB + Request)
444-
# We want History to fill gaps, so History is the base.
445-
# But wait, user_profile already has DB+Request.
446-
# So: final = {**history_profile, **user_profile}
447-
# This ensures DB/Request values (verified/explicit) override History.
448450
user_profile = {**history_profile, **user_profile}
449451
except Exception as e:
450-
print(f"⚠️ History merge failed: {e}")
452+
print(f"Warning: history merge failed: {e}")
451453
except Exception as e:
452-
print(f"⚠️ Auth lookup failed: {e}")
453-
454+
print(f"Warning: auth lookup failed: {e}")
455+
456+
# Cache check (per user + form URL, TTL 24h)
457+
cache_key = None
458+
if data.form_url:
459+
url_hash = hashlib.md5(data.form_url.encode()).hexdigest()
460+
cache_key = f"magic_fill:{cache_user_id}:{url_hash}"
461+
cached_result = await get_cached(cache_key)
462+
if cached_result:
463+
print(f"Magic Fill cache hit for {data.form_url}")
464+
cached_dict = cached_result if isinstance(cached_result, dict) else json.loads(cached_result)
465+
for fname, val in cached_dict.get("filled", {}).items():
466+
await emit_field_filled(fname, val, client_id)
467+
return {**cached_dict, "cached": True}
468+
454469
if not user_profile:
455470
return {
456471
"success": False,
@@ -459,22 +474,27 @@ async def magic_fill(
459474
"unfilled": [],
460475
"summary": "Please sign in to use Magic Fill"
461476
}
462-
463-
# 2. Call Smart Form Filler Chain
477+
464478
if not gemini_service:
465479
raise HTTPException(status_code=500, detail="Gemini service not available")
466-
480+
467481
filler = SmartFormFillerChain(gemini_service.llm)
482+
483+
async def progress_cb(field_id: str, value: Any):
484+
await emit_field_filled(field_id, value, client_id)
485+
468486
result = await filler.fill(
469487
user_profile=user_profile,
470488
form_schema=data.form_schema,
471-
min_confidence=0.5
489+
min_confidence=0.5,
490+
progress_cb=progress_cb
472491
)
473-
474-
print(f"✨ Magic Fill: {len(result.get('filled', {}))} fields filled")
475-
492+
493+
if cache_key and result.get("success"):
494+
await set_cached(cache_key, result, ttl=86400)
495+
476496
return result
477-
497+
478498
except Exception as e:
479499
import traceback
480500
traceback.print_exc()

0 commit comments

Comments
 (0)