Skip to content

Commit 6a2820a

Browse files
committed
sync: pull latest backend from schemasense repo
- Activity tracking, chat caching, PDF export, noise reduction
1 parent 70fb1a3 commit 6a2820a

12 files changed

Lines changed: 495 additions & 10 deletions

File tree

backend/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from fastapi.middleware.cors import CORSMiddleware
55
from config import get_settings
66
from utils.database import db
7-
from routes import tables, chat, chat_stream, export, auth, connection, dashboard, profile, settings as settings_routes, integrations, cache as cache_routes
8-
from utils.cache_db import init_cache
7+
from routes import tables, chat, chat_stream, export, auth, connection, dashboard, profile, settings as settings_routes, integrations, cache as cache_routes, activity
8+
from utils.cache_db import init_cache, init_chat_cache
99

1010
# Setup logging
1111
logging.basicConfig(
@@ -25,6 +25,7 @@ async def lifespan(app: FastAPI):
2525
logger.info("Starting SchemaSense Backend...")
2626
logger.info(f"Binding to {settings.API_HOST}:{settings.API_PORT}")
2727
await init_cache()
28+
await init_chat_cache()
2829
logger.info("Backend ready! (Database connection will be established per user via /api/connect-db)")
2930

3031
yield
@@ -67,6 +68,7 @@ async def lifespan(app: FastAPI):
6768
app.include_router(settings_routes.router)
6869
app.include_router(integrations.router)
6970
app.include_router(cache_routes.router)
71+
app.include_router(activity.router)
7072

7173
# ===== Root Routes =====
7274

backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ bcrypt==4.1.1
1010
PyJWT==2.11.0
1111
python-dotenv==1.0.0
1212
pymssql>=2.2.8
13+
fpdf2>=2.7.0

backend/routes/activity.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from fastapi import APIRouter
2+
from typing import Optional
3+
from utils.activity import get_recent_activities
4+
import logging
5+
6+
logger = logging.getLogger(__name__)
7+
8+
router = APIRouter(prefix="/api", tags=["activity"])
9+
10+
11+
@router.get("/activity/recent")
12+
async def recent_activity(limit: Optional[int] = 20):
13+
"""Get recent activity feed"""
14+
activities = get_recent_activities(limit=limit or 20)
15+
return {"activities": activities, "total": len(activities)}

backend/routes/chat.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from schemas import ChatRequest, ChatResponse
55
from routes.connection import get_user_db, get_db_pool, get_schema_filter_for
66
from utils.deepseek_client import deepseek_client
7+
from utils.activity import log_activity, ActivityType
78
import logging
89

910
logger = logging.getLogger(__name__)
@@ -45,6 +46,13 @@ async def chat_with_schema(request: ChatRequestWithConnection):
4546
context=context
4647
)
4748

49+
log_activity(
50+
ActivityType.CHAT_QUERY,
51+
"Chat query",
52+
f"Asked: {request.question[:80]}{'...' if len(request.question) > 80 else ''}",
53+
{"question": request.question[:200]}
54+
)
55+
4856
return ChatResponse(
4957
question=request.question,
5058
answer=answer,

backend/routes/chat_stream.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from fastapi import APIRouter, HTTPException, Request
22
from fastapi.responses import StreamingResponse
33
from schemas import ChatRequest
4-
from routes.connection import get_user_db, get_db_pool, get_schema_filter_for, get_db_type
4+
from routes.connection import get_user_db, get_db_pool, get_schema_filter_for, get_db_type, get_connection_details
55
from utils.deepseek_client import deepseek_client
66
from utils.schema_queries import get_count_query
7+
from utils.activity import log_activity, ActivityType
8+
from utils.cache_db import get_cached_chat_response, store_chat_response
79
import logging
810
import json
911
from typing import Optional
@@ -26,6 +28,30 @@ async def chat_with_schema_streaming(request: ChatStreamRequest):
2628
try:
2729
db = get_db_pool(request.connection_id) if request.connection_id else get_user_db()
2830

31+
# Check cache first
32+
conn_details = get_connection_details(request.connection_id)
33+
if conn_details:
34+
cached = await get_cached_chat_response(
35+
host=conn_details["host"],
36+
port=conn_details["port"],
37+
database=conn_details["database"],
38+
schema_filter=conn_details["schema_filter"],
39+
user=conn_details["user"],
40+
question=request.question,
41+
)
42+
if cached:
43+
log_activity(
44+
ActivityType.CHAT_QUERY,
45+
"Chat query (cached)",
46+
f"Asked: {request.question[:80]}{'...' if len(request.question) > 80 else ''}",
47+
{"question": request.question[:200], "cached": True}
48+
)
49+
async def cached_response():
50+
yield json.dumps({"type": "status", "message": "⚡ Found cached response..."}) + "\n"
51+
yield json.dumps({"type": "content", "data": cached}) + "\n"
52+
yield json.dumps({"type": "done"}) + "\n"
53+
return StreamingResponse(cached_response(), media_type="application/x-ndjson")
54+
2955
# Build comprehensive context from all tables
3056
async with db.acquire() as conn:
3157
tables_query = """
@@ -40,7 +66,6 @@ async def chat_with_schema_streaming(request: ChatStreamRequest):
4066
context = "DATABASE SCHEMA:\n"
4167
db_type = get_db_type(request.connection_id)
4268
for table_name in table_names:
43-
# Get row count
4469
count_query = get_count_query(table_name, db_type)
4570
count_result = await conn.fetchval(count_query)
4671

@@ -54,30 +79,49 @@ async def chat_with_schema_streaming(request: ChatStreamRequest):
5479

5580
async def stream_response():
5681
"""Generator that streams response chunks"""
57-
# Send initial status that appears in blue box
82+
log_activity(
83+
ActivityType.CHAT_QUERY,
84+
"Chat query (streaming)",
85+
f"Asked: {request.question[:80]}{'...' if len(request.question) > 80 else ''}",
86+
{"question": request.question[:200]}
87+
)
5888
yield json.dumps({"type": "status", "message": "🤖 Connecting to AI..."}) + "\n"
5989
yield json.dumps({"type": "status", "message": "📊 Analyzing your database..."}) + "\n"
6090
yield json.dumps({"type": "status", "message": "🔍 Processing your question..."}) + "\n"
6191
yield json.dumps({"type": "status", "message": "⚡ Generating response..."}) + "\n"
6292

6393
try:
64-
# Stream the actual response content (without progress messages)
94+
full_response = ""
6595
async for chunk in deepseek_client.stream_chat_about_schema(
6696
question=request.question,
6797
context=context
6898
):
69-
# Send content chunks directly (no status messages mixed in)
99+
full_response += chunk
70100
yield json.dumps({"type": "content", "data": chunk}) + "\n"
71101

72-
# Completion signal
102+
# Store in cache for future requests
103+
if conn_details and full_response:
104+
try:
105+
await store_chat_response(
106+
host=conn_details["host"],
107+
port=conn_details["port"],
108+
database=conn_details["database"],
109+
schema_filter=conn_details["schema_filter"],
110+
user=conn_details["user"],
111+
question=request.question,
112+
response=full_response,
113+
)
114+
except Exception as cache_err:
115+
logger.warning(f"Failed to cache chat response: {cache_err}")
116+
73117
yield json.dumps({"type": "done"}) + "\n"
74118
except Exception as e:
75119
logger.error(f"Streaming error: {e}")
76120
yield json.dumps({"type": "error", "message": f"Error: {str(e)}"}) + "\n"
77121

78122
return StreamingResponse(
79123
stream_response(),
80-
media_type="application/x-ndjson" # Newline-delimited JSON
124+
media_type="application/x-ndjson"
81125
)
82126

83127
except HTTPException:

backend/routes/connection.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import uuid
66
from typing import Dict, Optional, List
77
from datetime import datetime
8+
from utils.activity import log_activity, ActivityType
89

910
logger = logging.getLogger(__name__)
1011
router = APIRouter(prefix="/api", tags=["connection"])
@@ -217,6 +218,13 @@ async def connect_database(request: ConnectionRequest, response: Response):
217218

218219
logger.info(f"Connected [{conn_id}] ({request.database_type}): {request.host}:{request.port}/{request.database}")
219220

221+
log_activity(
222+
ActivityType.DATABASE_CONNECTED,
223+
f"Connected to {conn_name}",
224+
f"Database {request.database} on {request.host}:{request.port} ({request.database_type})",
225+
{"connection_id": conn_id, "database": request.database, "host": request.host}
226+
)
227+
220228
# Set a cookie
221229
response.set_cookie(
222230
key="db_connected", value="true",
@@ -295,6 +303,12 @@ async def activate_connection(connection_id: str):
295303
active_connection_id = connection_id
296304
_sync_user_db_connection()
297305
logger.info(f"Activated connection: {connection_id}")
306+
log_activity(
307+
ActivityType.DATABASE_ACTIVATED,
308+
f"Activated {connections[connection_id].get('name', connection_id)}",
309+
f"Switched active database to {connections[connection_id]['database']}",
310+
{"connection_id": connection_id}
311+
)
298312
return {"success": True, "message": f"Connection '{connection_id}' is now active", "active_id": connection_id}
299313

300314
@router.delete("/connections/{connection_id}")
@@ -322,6 +336,12 @@ async def remove_connection(connection_id: str):
322336
_sync_user_db_connection()
323337

324338
logger.info(f"Removed connection: {connection_id}")
339+
log_activity(
340+
ActivityType.DATABASE_DISCONNECTED,
341+
f"Disconnected database",
342+
f"Removed database connection {connection_id}",
343+
{"connection_id": connection_id}
344+
)
325345
return {"success": True, "message": "Connection removed", "active_id": active_connection_id}
326346

327347
@router.post("/disconnect-db")

backend/routes/dashboard.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import List, Optional
44
from routes import connection as conn_module
55
from routes.connection import get_user_db, get_db_pool, get_schema_filter_for
6+
from utils.activity import get_analyses_count
67
from pydantic import BaseModel
78
import logging
89

@@ -116,7 +117,7 @@ async def get_statistics():
116117
return {
117118
"connectedDatabases": len(conn_module.connections),
118119
"totalTables": total_tables,
119-
"analysesRun": len(conn_module.connections)
120+
"analysesRun": get_analyses_count()
120121
}
121122
except HTTPException:
122123
raise

backend/routes/export.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from fastapi import APIRouter, HTTPException
2+
from fastapi.responses import Response
23
from datetime import datetime
34
from typing import Optional
45
from schemas import ExportResponse
56
from routes.connection import get_user_db, get_db_pool, get_schema_filter_for
67
from routes.tables import get_schema, get_data_quality, explain_table
8+
from utils.pdf_generator import generate_table_pdf
79
import logging
810

911
logger = logging.getLogger(__name__)
@@ -68,3 +70,41 @@ async def export_documentation(table_name: str, connection_id: Optional[str] = N
6870
except Exception as e:
6971
logger.error(f"Error exporting {table_name}: {e}")
7072
raise HTTPException(status_code=500, detail=f"Export error: {str(e)}")
73+
74+
75+
@router.get("/export/{table_name}/pdf")
76+
async def export_pdf(table_name: str, connection_id: Optional[str] = None):
77+
"""Export table documentation as a PDF report (reuses cached AI analysis)"""
78+
try:
79+
db = get_db_pool(connection_id) if connection_id else get_user_db()
80+
schema = get_schema_filter_for(connection_id)
81+
async with db.acquire() as conn:
82+
tables = await conn.fetch("""
83+
SELECT table_name FROM information_schema.tables
84+
WHERE table_schema=$1 AND table_type='BASE TABLE'
85+
""", schema)
86+
table_names = [t['table_name'] for t in tables]
87+
if table_name not in table_names:
88+
raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found")
89+
90+
schema_info = await get_schema(table_name, connection_id=connection_id)
91+
quality = await get_data_quality(table_name, connection_id=connection_id)
92+
explanation = await explain_table(table_name, connection_id=connection_id)
93+
94+
pdf_bytes = generate_table_pdf(
95+
table_name=table_name,
96+
schema_info=schema_info,
97+
quality=quality,
98+
business_context=explanation.business_explanation,
99+
)
100+
101+
return Response(
102+
content=pdf_bytes,
103+
media_type="application/pdf",
104+
headers={"Content-Disposition": f'attachment; filename="{table_name}_report.pdf"'},
105+
)
106+
except HTTPException:
107+
raise
108+
except Exception as e:
109+
logger.error(f"Error generating PDF for {table_name}: {e}")
110+
raise HTTPException(status_code=500, detail=f"PDF export error: {str(e)}")

backend/routes/tables.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from utils.deepseek_client import deepseek_client
66
from utils.schema_queries import get_sample_query, get_count_query, get_count_alias_query, get_null_stats_query
77
from utils.cache_db import get_cached_explanation, store_explanation
8+
from utils.activity import log_activity, ActivityType
89
from datetime import datetime
910
import logging
1011

@@ -192,6 +193,13 @@ async def get_data_quality(table_name: str, connection_id: Optional[str] = None)
192193
overall_score=round(overall_score, 1)
193194
)
194195

196+
log_activity(
197+
ActivityType.ANALYSIS_RUN,
198+
f"Quality analysis: {table_name}",
199+
f"Data quality score: {quality_grade} ({round(avg_completeness, 1)}% completeness)",
200+
{"table_name": table_name, "grade": quality_grade}
201+
)
202+
195203
return TableQuality(
196204
table_name=table_name,
197205
row_count=total_count,
@@ -271,6 +279,13 @@ async def explain_table(table_name: str, connection_id: Optional[str] = None):
271279
row_count=schema_info.row_count,
272280
)
273281

282+
log_activity(
283+
ActivityType.TABLE_EXPLAINED,
284+
f"AI explained: {table_name}",
285+
f"Generated AI business explanation for {table_name}",
286+
{"table_name": table_name}
287+
)
288+
274289
return TableExplanation(
275290
table_name=table_name,
276291
business_explanation=explanation,

0 commit comments

Comments
 (0)