Patoliya Chirag Cursor commited on
Commit
3b7f6d7
·
1 Parent(s): 3efb887

Trinetra AI rebrand, Python 3.14 compatibility, local run support

Browse files

- UI: Rebrand ScamShield AI to Trinetra AI; add tagline Detect. Engage. Expose.
- Backend: Avoid loading SQLAlchemy when POSTGRES_URL unset (Python 3.14 typing fix)
- Extractor: Fallback to regex-only when spaCy fails; resilient get_extractor()
- API docs: Update title/description to Trinetra AI
- Add requirements-local.txt for local Python 3.11+ install

Co-authored-by: Cursor <cursoragent@cursor.com>

app/api/endpoints.py CHANGED
@@ -186,27 +186,27 @@ async def engage_honeypot(request_body: Dict[str, Any] = Body(default={})) -> En
186
  # Save session state to Redis (with in-memory fallback)
187
  save_session_state_with_fallback(session_id, result)
188
 
189
- # Save conversation to PostgreSQL (optional, graceful degradation)
190
- try:
191
- from app.database.postgres import save_conversation
192
-
193
- conversation_data = {
194
- "language": detected_language,
195
- "persona": result.get("persona"),
196
- "scam_confidence": confidence,
197
- "turn_count": result.get("turn_count", 1),
198
- "messages": result.get("messages", []),
199
- "extracted_intel": intel,
200
- "extraction_confidence": extraction_confidence,
201
- }
202
-
203
- conversation_id = save_conversation(session_id, conversation_data)
204
- if conversation_id > 0:
205
- logger.debug(f"Conversation saved to PostgreSQL: id={conversation_id}")
206
- except Exception as e:
207
- # PostgreSQL save failed, but continue - Redis already saved the session
208
- logger.warning(f"Failed to save conversation to PostgreSQL: {e}")
209
- logger.info("Session state saved to Redis, continuing without PostgreSQL persistence")
210
 
211
  # Build conversation history for response
212
  conversation_history_response = []
@@ -397,11 +397,10 @@ async def get_session(session_id: str) -> SessionResponse:
397
 
398
  try:
399
  from app.database.redis_client import get_session_state_with_fallback
400
- from app.database.postgres import get_conversation
401
-
402
  # Try Redis first (active sessions)
403
  session_state = get_session_state_with_fallback(session_id)
404
-
405
  if session_state:
406
  # Build response from Redis session state
407
  messages = session_state.get("messages", [])
@@ -451,9 +450,15 @@ async def get_session(session_id: str) -> SessionResponse:
451
  updated_at=updated_at,
452
  )
453
 
454
- # Try PostgreSQL for archived sessions
455
- conversation = get_conversation(session_id)
456
-
 
 
 
 
 
 
457
  if conversation:
458
  messages = conversation.get("messages", [])
459
  conversation_history = []
@@ -545,13 +550,14 @@ async def health_check() -> HealthResponse:
545
  logger.warning(f"Redis health check failed: {e}")
546
  redis_status = "offline"
547
 
548
- # Check PostgreSQL
549
- try:
550
- from app.database.postgres import verify_schema
551
- postgres_status = "online" if verify_schema() else "degraded"
552
- except Exception as e:
553
- logger.warning(f"PostgreSQL health check failed: {e}")
554
- postgres_status = "offline"
 
555
 
556
  # Check Groq API (just check if API key is configured)
557
  try:
 
186
  # Save session state to Redis (with in-memory fallback)
187
  save_session_state_with_fallback(session_id, result)
188
 
189
+ # Save conversation to PostgreSQL only when configured (avoids loading SQLAlchemy on Python 3.14 when unused)
190
+ if settings.POSTGRES_URL:
191
+ try:
192
+ from app.database.postgres import save_conversation
193
+
194
+ conversation_data = {
195
+ "language": detected_language,
196
+ "persona": result.get("persona"),
197
+ "scam_confidence": confidence,
198
+ "turn_count": result.get("turn_count", 1),
199
+ "messages": result.get("messages", []),
200
+ "extracted_intel": intel,
201
+ "extraction_confidence": extraction_confidence,
202
+ }
203
+
204
+ conversation_id = save_conversation(session_id, conversation_data)
205
+ if conversation_id > 0:
206
+ logger.debug(f"Conversation saved to PostgreSQL: id={conversation_id}")
207
+ except Exception as e:
208
+ logger.warning(f"Failed to save conversation to PostgreSQL: {e}")
209
+ logger.info("Session state saved to Redis, continuing without PostgreSQL persistence")
210
 
211
  # Build conversation history for response
212
  conversation_history_response = []
 
397
 
398
  try:
399
  from app.database.redis_client import get_session_state_with_fallback
400
+
 
401
  # Try Redis first (active sessions)
402
  session_state = get_session_state_with_fallback(session_id)
403
+
404
  if session_state:
405
  # Build response from Redis session state
406
  messages = session_state.get("messages", [])
 
450
  updated_at=updated_at,
451
  )
452
 
453
+ # Try PostgreSQL for archived sessions (only when configured; avoids loading SQLAlchemy when unused)
454
+ conversation = None
455
+ if settings.POSTGRES_URL:
456
+ try:
457
+ from app.database.postgres import get_conversation
458
+ conversation = get_conversation(session_id)
459
+ except Exception as e:
460
+ logger.warning(f"Failed to get conversation from PostgreSQL: {e}")
461
+
462
  if conversation:
463
  messages = conversation.get("messages", [])
464
  conversation_history = []
 
550
  logger.warning(f"Redis health check failed: {e}")
551
  redis_status = "offline"
552
 
553
+ # Check PostgreSQL (only when configured; avoids loading SQLAlchemy on Python 3.14 when unused)
554
+ if settings.POSTGRES_URL:
555
+ try:
556
+ from app.database.postgres import verify_schema
557
+ postgres_status = "online" if verify_schema() else "degraded"
558
+ except Exception as e:
559
+ logger.warning(f"PostgreSQL health check failed: {e}")
560
+ postgres_status = "offline"
561
 
562
  # Check Groq API (just check if API key is configured)
563
  try:
app/database/__init__.py CHANGED
@@ -1,126 +1,85 @@
1
- """
2
- Database Layer - Storage backends for conversations, sessions, and embeddings.
3
-
4
- This module provides:
5
- - PostgreSQL: Conversation logs and extracted intelligence
6
- - Redis: Session state management with TTL
7
- - ChromaDB: Vector embeddings for semantic search
8
-
9
- Task 6.2 Implementation:
10
- - AC-2.3.1: State persists across API calls
11
- - AC-2.3.2: Session expires after 1 hour
12
- - AC-2.3.3: PostgreSQL stores complete logs
13
- - AC-2.3.4: Redis failure degrades gracefully
14
- """
15
-
16
- from app.database.postgres import (
17
- # Connection management
18
- get_db_connection,
19
- get_db_session,
20
- init_engine,
21
- init_database,
22
- verify_schema,
23
- SessionLocal,
24
- # Conversation CRUD
25
- save_conversation,
26
- get_conversation,
27
- update_conversation,
28
- delete_conversation,
29
- get_conversations_by_date,
30
- get_conversation_stats,
31
- # Messages
32
- save_messages,
33
- # Intelligence
34
- save_intelligence,
35
- get_scammer_profiles,
36
- )
37
- from app.database.redis_client import (
38
- # Connection management
39
- get_redis_client,
40
- init_redis_client,
41
- health_check,
42
- is_redis_available,
43
- # Session state management
44
- save_session_state,
45
- get_session_state,
46
- delete_session_state,
47
- update_session_state,
48
- # Graceful degradation with fallback
49
- save_session_state_with_fallback,
50
- get_session_state_with_fallback,
51
- delete_session_state_with_fallback,
52
- # Session utilities
53
- extend_session_ttl,
54
- get_session_ttl,
55
- get_active_session_count,
56
- clear_all_sessions,
57
- reset_fallback_cache,
58
- get_fallback_cache_stats,
59
- # Rate limiting
60
- increment_rate_counter,
61
- check_rate_limit,
62
- # Constants
63
- DEFAULT_SESSION_TTL,
64
- )
65
- from app.database.chromadb_client import (
66
- get_chromadb_client,
67
- store_embedding,
68
- search_similar,
69
- )
70
- from app.database.models import Conversation, Message, ExtractedIntelligence
71
-
72
- __all__ = [
73
- # PostgreSQL - Connection
74
- "get_db_connection",
75
- "get_db_session",
76
- "init_engine",
77
- "init_database",
78
- "verify_schema",
79
- "SessionLocal",
80
- # PostgreSQL - Conversation CRUD
81
- "save_conversation",
82
- "get_conversation",
83
- "update_conversation",
84
- "delete_conversation",
85
- "get_conversations_by_date",
86
- "get_conversation_stats",
87
- # PostgreSQL - Messages
88
- "save_messages",
89
- # PostgreSQL - Intelligence
90
- "save_intelligence",
91
- "get_scammer_profiles",
92
- # Redis - Connection
93
- "get_redis_client",
94
- "init_redis_client",
95
- "health_check",
96
- "is_redis_available",
97
- # Redis - Session state
98
- "save_session_state",
99
- "get_session_state",
100
- "delete_session_state",
101
- "update_session_state",
102
- # Redis - Graceful degradation
103
- "save_session_state_with_fallback",
104
- "get_session_state_with_fallback",
105
- "delete_session_state_with_fallback",
106
- # Redis - Utilities
107
- "extend_session_ttl",
108
- "get_session_ttl",
109
- "get_active_session_count",
110
- "clear_all_sessions",
111
- "reset_fallback_cache",
112
- "get_fallback_cache_stats",
113
- # Redis - Rate limiting
114
- "increment_rate_counter",
115
- "check_rate_limit",
116
- # Redis - Constants
117
- "DEFAULT_SESSION_TTL",
118
- # ChromaDB
119
- "get_chromadb_client",
120
- "store_embedding",
121
- "search_similar",
122
- # Models
123
- "Conversation",
124
- "Message",
125
- "ExtractedIntelligence",
126
- ]
 
1
+ """
2
+ Database Layer - Storage backends for conversations, sessions, and embeddings.
3
+
4
+ This module provides:
5
+ - PostgreSQL: Conversation logs and extracted intelligence
6
+ - Redis: Session state management with TTL
7
+ - ChromaDB: Vector embeddings for semantic search
8
+
9
+ Task 6.2 Implementation:
10
+ - AC-2.3.1: State persists across API calls
11
+ - AC-2.3.2: Session expires after 1 hour
12
+ - AC-2.3.3: PostgreSQL stores complete logs
13
+ - AC-2.3.4: Redis failure degrades gracefully
14
+
15
+ Note: PostgreSQL is not imported here to avoid loading SQLAlchemy when POSTGRES_URL
16
+ is unset (Python 3.14 typing compatibility). Import from app.database.postgres directly
17
+ when POSTGRES_URL is configured.
18
+ """
19
+
20
+ from app.database.redis_client import (
21
+ # Connection management
22
+ get_redis_client,
23
+ init_redis_client,
24
+ health_check,
25
+ is_redis_available,
26
+ # Session state management
27
+ save_session_state,
28
+ get_session_state,
29
+ delete_session_state,
30
+ update_session_state,
31
+ # Graceful degradation with fallback
32
+ save_session_state_with_fallback,
33
+ get_session_state_with_fallback,
34
+ delete_session_state_with_fallback,
35
+ # Session utilities
36
+ extend_session_ttl,
37
+ get_session_ttl,
38
+ get_active_session_count,
39
+ clear_all_sessions,
40
+ reset_fallback_cache,
41
+ get_fallback_cache_stats,
42
+ # Rate limiting
43
+ increment_rate_counter,
44
+ check_rate_limit,
45
+ # Constants
46
+ DEFAULT_SESSION_TTL,
47
+ )
48
+ from app.database.chromadb_client import (
49
+ get_chromadb_client,
50
+ store_embedding,
51
+ search_similar,
52
+ )
53
+
54
+ __all__ = [
55
+ # Redis - Connection
56
+ "get_redis_client",
57
+ "init_redis_client",
58
+ "health_check",
59
+ "is_redis_available",
60
+ # Redis - Session state
61
+ "save_session_state",
62
+ "get_session_state",
63
+ "delete_session_state",
64
+ "update_session_state",
65
+ # Redis - Graceful degradation
66
+ "save_session_state_with_fallback",
67
+ "get_session_state_with_fallback",
68
+ "delete_session_state_with_fallback",
69
+ # Redis - Utilities
70
+ "extend_session_ttl",
71
+ "get_session_ttl",
72
+ "get_active_session_count",
73
+ "clear_all_sessions",
74
+ "reset_fallback_cache",
75
+ "get_fallback_cache_stats",
76
+ # Redis - Rate limiting
77
+ "increment_rate_counter",
78
+ "check_rate_limit",
79
+ # Redis - Constants
80
+ "DEFAULT_SESSION_TTL",
81
+ # ChromaDB
82
+ "get_chromadb_client",
83
+ "store_embedding",
84
+ "search_similar",
85
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/main.py CHANGED
@@ -1,260 +1,260 @@
1
- """
2
- FastAPI Application Entry Point.
3
-
4
- ScamShield AI - Agentic Honeypot for Scam Detection and Intelligence Extraction.
5
-
6
- This module creates and configures the FastAPI application with:
7
- - API routes for honeypot endpoints
8
- - CORS middleware
9
- - Exception handlers
10
- - Startup/shutdown events
11
- """
12
-
13
- from contextlib import asynccontextmanager
14
- from datetime import datetime
15
- import time
16
-
17
- from fastapi import FastAPI, Request
18
- from fastapi.middleware.cors import CORSMiddleware
19
- from fastapi.responses import JSONResponse
20
- from fastapi.staticfiles import StaticFiles
21
- from pathlib import Path
22
-
23
- from app.config import settings
24
- from app.api.endpoints import router
25
- from app.utils.logger import setup_logging, get_logger
26
-
27
- # Initialize logging
28
- setup_logging(level=settings.LOG_LEVEL)
29
- logger = get_logger(__name__)
30
-
31
- # Track startup time for uptime calculation
32
- _startup_time: float = 0
33
-
34
-
35
- @asynccontextmanager
36
- async def lifespan(app: FastAPI):
37
- """
38
- Application lifespan manager.
39
-
40
- Handles startup and shutdown events.
41
- """
42
- global _startup_time
43
-
44
- # Startup
45
- logger.info("Starting ScamShield AI...")
46
- _startup_time = time.time()
47
-
48
- # Pre-load ML models (prevents cold-start delays)
49
- logger.info("Loading ML models...")
50
- try:
51
- from app.models.detector import get_detector
52
- from app.models.extractor import get_extractor
53
-
54
- # Pre-initialize detector (loads IndicBERT)
55
- detector = get_detector()
56
- logger.info(f"Detector ready (model loaded: {detector._model_loaded})")
57
-
58
- # Pre-initialize extractor (loads spaCy)
59
- extractor = get_extractor()
60
- logger.info(f"Extractor ready (spaCy loaded: {extractor.nlp is not None})")
61
-
62
- logger.info("All ML models loaded successfully")
63
- except Exception as e:
64
- logger.error(f"Failed to load ML models: {e}")
65
- logger.warning("Application will continue but may have degraded functionality")
66
-
67
- # Initialize PostgreSQL database
68
- if settings.POSTGRES_URL:
69
- try:
70
- from app.database.postgres import init_engine, init_database, verify_schema
71
-
72
- logger.info("Initializing PostgreSQL connection...")
73
- init_engine()
74
-
75
- # Initialize database schema if needed
76
- if not verify_schema():
77
- logger.info("Database schema not found, initializing...")
78
- init_database()
79
- else:
80
- logger.info("Database schema verified")
81
-
82
- logger.info("PostgreSQL initialized successfully")
83
- except Exception as e:
84
- logger.error(f"Failed to initialize PostgreSQL: {e}")
85
- logger.warning("PostgreSQL operations will fail. Application will continue with Redis only.")
86
- else:
87
- logger.warning("POSTGRES_URL not configured. PostgreSQL features disabled.")
88
-
89
- # Initialize Redis connection
90
- if settings.REDIS_URL:
91
- try:
92
- from app.database.redis_client import init_redis_client, is_redis_available
93
-
94
- logger.info("Initializing Redis connection...")
95
- init_redis_client()
96
-
97
- if is_redis_available():
98
- logger.info("Redis initialized successfully")
99
- else:
100
- logger.warning("Redis connection failed. Will use in-memory fallback.")
101
- except Exception as e:
102
- logger.error(f"Failed to initialize Redis: {e}")
103
- logger.warning("Redis operations will fail. Will use in-memory fallback.")
104
- else:
105
- logger.warning("REDIS_URL not configured. Will use in-memory session storage.")
106
-
107
- logger.info(f"ScamShield AI started in {settings.ENVIRONMENT} mode")
108
-
109
- yield
110
-
111
- # Shutdown
112
- logger.info("Shutting down ScamShield AI...")
113
-
114
- # Cleanup resources
115
- try:
116
- from app.database.postgres import engine
117
- if engine:
118
- engine.dispose()
119
- logger.info("PostgreSQL connections closed")
120
- except Exception as e:
121
- logger.warning(f"Error closing PostgreSQL connections: {e}")
122
-
123
- try:
124
- from app.database.redis_client import redis_client
125
- if redis_client:
126
- redis_client.close()
127
- logger.info("Redis connection closed")
128
- except Exception as e:
129
- logger.warning(f"Error closing Redis connection: {e}")
130
-
131
- logger.info("ScamShield AI shutdown complete")
132
-
133
-
134
- # Create FastAPI application
135
- app = FastAPI(
136
- title="ScamShield AI",
137
- description="Agentic Honeypot System for Scam Detection & Intelligence Extraction",
138
- version="1.0.0",
139
- lifespan=lifespan,
140
- docs_url="/docs" if settings.DEBUG else None,
141
- redoc_url="/redoc" if settings.DEBUG else None,
142
- )
143
-
144
- # Configure CORS
145
- app.add_middleware(
146
- CORSMiddleware,
147
- allow_origins=["*"], # TODO: Restrict in production
148
- allow_credentials=True,
149
- allow_methods=["*"],
150
- allow_headers=["*"],
151
- )
152
-
153
- # Include Phase 1 API routes
154
- app.include_router(router)
155
-
156
- # Conditionally include Phase 2 voice routes (opt-in, default disabled)
157
- if getattr(settings, "PHASE_2_ENABLED", False):
158
- try:
159
- from app.api.voice_endpoints import voice_router
160
- app.include_router(voice_router)
161
- logger.info("Phase 2 voice endpoints enabled")
162
- except ImportError as e:
163
- logger.warning(f"Phase 2 voice endpoints unavailable (missing dependencies): {e}")
164
- except Exception as e:
165
- logger.error(f"Failed to load Phase 2 voice endpoints: {e}")
166
- else:
167
- logger.info("Phase 2 voice features disabled (PHASE_2_ENABLED=false)")
168
-
169
- # Mount static files for UI
170
- ui_path = Path(__file__).parent.parent / "ui"
171
- if ui_path.exists():
172
- app.mount("/ui", StaticFiles(directory=str(ui_path), html=True), name="ui")
173
- logger.info(f"UI mounted at /ui (from {ui_path})")
174
-
175
- # Serve index.html at root
176
- @app.get("/", include_in_schema=False)
177
- async def serve_ui():
178
- """Serve the UI dashboard at root."""
179
- from fastapi.responses import FileResponse
180
- index_file = ui_path / "index.html"
181
- if index_file.exists():
182
- return FileResponse(index_file)
183
- return {"message": "UI files not found"}
184
-
185
- # Serve GUVI Tester at /guvi-test
186
- @app.get("/guvi-test", include_in_schema=False)
187
- async def serve_guvi_tester():
188
- """Serve the GUVI Format Tester UI."""
189
- from fastapi.responses import FileResponse
190
- guvi_test_file = ui_path / "guvi-test.html"
191
- if guvi_test_file.exists():
192
- return FileResponse(guvi_test_file)
193
- return {"message": "GUVI Tester UI not found"}
194
-
195
- # Serve Phase 2 Voice UI at /voice (only when Phase 2 is enabled)
196
- if getattr(settings, "PHASE_2_ENABLED", False):
197
- @app.get("/voice", include_in_schema=False)
198
- async def serve_voice_ui():
199
- """Serve the Phase 2 Voice Honeypot UI."""
200
- from fastapi.responses import FileResponse
201
- voice_file = ui_path / "voice.html"
202
- if voice_file.exists():
203
- return FileResponse(voice_file)
204
- return {"message": "Voice UI not found"}
205
-
206
-
207
- @app.exception_handler(Exception)
208
- async def global_exception_handler(request: Request, exc: Exception):
209
- """
210
- Global exception handler for unhandled errors.
211
-
212
- Args:
213
- request: FastAPI request
214
- exc: Exception that was raised
215
-
216
- Returns:
217
- JSON error response
218
- """
219
- logger.error(f"Unhandled exception: {exc}", exc_info=True)
220
-
221
- return JSONResponse(
222
- status_code=500,
223
- content={
224
- "status": "error",
225
- "error": {
226
- "code": "INTERNAL_ERROR",
227
- "message": "An unexpected error occurred while processing your request",
228
- "details": {
229
- "timestamp": datetime.utcnow().isoformat() + "Z",
230
- },
231
- },
232
- },
233
- )
234
-
235
-
236
- # Root endpoint moved to serve UI (see above)
237
-
238
-
239
- def get_uptime_seconds() -> int:
240
- """
241
- Get application uptime in seconds.
242
-
243
- Returns:
244
- Uptime in seconds
245
- """
246
- if _startup_time == 0:
247
- return 0
248
- return int(time.time() - _startup_time)
249
-
250
-
251
- # Export for uvicorn
252
- if __name__ == "__main__":
253
- import uvicorn
254
-
255
- uvicorn.run(
256
- "app.main:app",
257
- host=settings.API_HOST,
258
- port=settings.API_PORT,
259
- reload=settings.is_development,
260
- )
 
1
+ """
2
+ FastAPI Application Entry Point.
3
+
4
+ ScamShield AI - Agentic Honeypot for Scam Detection and Intelligence Extraction.
5
+
6
+ This module creates and configures the FastAPI application with:
7
+ - API routes for honeypot endpoints
8
+ - CORS middleware
9
+ - Exception handlers
10
+ - Startup/shutdown events
11
+ """
12
+
13
+ from contextlib import asynccontextmanager
14
+ from datetime import datetime
15
+ import time
16
+
17
+ from fastapi import FastAPI, Request
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from fastapi.responses import JSONResponse
20
+ from fastapi.staticfiles import StaticFiles
21
+ from pathlib import Path
22
+
23
+ from app.config import settings
24
+ from app.api.endpoints import router
25
+ from app.utils.logger import setup_logging, get_logger
26
+
27
+ # Initialize logging
28
+ setup_logging(level=settings.LOG_LEVEL)
29
+ logger = get_logger(__name__)
30
+
31
+ # Track startup time for uptime calculation
32
+ _startup_time: float = 0
33
+
34
+
35
+ @asynccontextmanager
36
+ async def lifespan(app: FastAPI):
37
+ """
38
+ Application lifespan manager.
39
+
40
+ Handles startup and shutdown events.
41
+ """
42
+ global _startup_time
43
+
44
+ # Startup
45
+ logger.info("Starting ScamShield AI...")
46
+ _startup_time = time.time()
47
+
48
+ # Pre-load ML models (prevents cold-start delays)
49
+ logger.info("Loading ML models...")
50
+ try:
51
+ from app.models.detector import get_detector
52
+ from app.models.extractor import get_extractor
53
+
54
+ # Pre-initialize detector (loads IndicBERT)
55
+ detector = get_detector()
56
+ logger.info(f"Detector ready (model loaded: {detector._model_loaded})")
57
+
58
+ # Pre-initialize extractor (loads spaCy)
59
+ extractor = get_extractor()
60
+ logger.info(f"Extractor ready (spaCy loaded: {extractor.nlp is not None})")
61
+
62
+ logger.info("All ML models loaded successfully")
63
+ except Exception as e:
64
+ logger.error(f"Failed to load ML models: {e}")
65
+ logger.warning("Application will continue but may have degraded functionality")
66
+
67
+ # Initialize PostgreSQL database
68
+ if settings.POSTGRES_URL:
69
+ try:
70
+ from app.database.postgres import init_engine, init_database, verify_schema
71
+
72
+ logger.info("Initializing PostgreSQL connection...")
73
+ init_engine()
74
+
75
+ # Initialize database schema if needed
76
+ if not verify_schema():
77
+ logger.info("Database schema not found, initializing...")
78
+ init_database()
79
+ else:
80
+ logger.info("Database schema verified")
81
+
82
+ logger.info("PostgreSQL initialized successfully")
83
+ except Exception as e:
84
+ logger.error(f"Failed to initialize PostgreSQL: {e}")
85
+ logger.warning("PostgreSQL operations will fail. Application will continue with Redis only.")
86
+ else:
87
+ logger.warning("POSTGRES_URL not configured. PostgreSQL features disabled.")
88
+
89
+ # Initialize Redis connection
90
+ if settings.REDIS_URL:
91
+ try:
92
+ from app.database.redis_client import init_redis_client, is_redis_available
93
+
94
+ logger.info("Initializing Redis connection...")
95
+ init_redis_client()
96
+
97
+ if is_redis_available():
98
+ logger.info("Redis initialized successfully")
99
+ else:
100
+ logger.warning("Redis connection failed. Will use in-memory fallback.")
101
+ except Exception as e:
102
+ logger.error(f"Failed to initialize Redis: {e}")
103
+ logger.warning("Redis operations will fail. Will use in-memory fallback.")
104
+ else:
105
+ logger.warning("REDIS_URL not configured. Will use in-memory session storage.")
106
+
107
+ logger.info(f"ScamShield AI started in {settings.ENVIRONMENT} mode")
108
+
109
+ yield
110
+
111
+ # Shutdown
112
+ logger.info("Shutting down ScamShield AI...")
113
+
114
+ if settings.POSTGRES_URL:
115
+ try:
116
+ from app.database.postgres import engine
117
+ if engine:
118
+ engine.dispose()
119
+ logger.info("PostgreSQL connections closed")
120
+ except Exception as e:
121
+ logger.warning(f"Error closing PostgreSQL connections: {e}")
122
+
123
+ try:
124
+ from app.database.redis_client import redis_client
125
+ if redis_client:
126
+ redis_client.close()
127
+ logger.info("Redis connection closed")
128
+ except Exception as e:
129
+ logger.warning(f"Error closing Redis connection: {e}")
130
+
131
+ logger.info("ScamShield AI shutdown complete")
132
+
133
+
134
+ # Create FastAPI application
135
+ app = FastAPI(
136
+ title="Trinetra AI",
137
+ description="Detect. Engage. Expose. Agentic Honeypot for Scam Detection and Intelligence Extraction.",
138
+ version="1.0.0",
139
+ lifespan=lifespan,
140
+ docs_url="/docs" if settings.DEBUG else None,
141
+ redoc_url="/redoc" if settings.DEBUG else None,
142
+ )
143
+
144
+ # Configure CORS
145
+ app.add_middleware(
146
+ CORSMiddleware,
147
+ allow_origins=["*"], # TODO: Restrict in production
148
+ allow_credentials=True,
149
+ allow_methods=["*"],
150
+ allow_headers=["*"],
151
+ )
152
+
153
+ # Include Phase 1 API routes
154
+ app.include_router(router)
155
+
156
+ # Conditionally include Phase 2 voice routes (opt-in, default disabled)
157
+ if getattr(settings, "PHASE_2_ENABLED", False):
158
+ try:
159
+ from app.api.voice_endpoints import voice_router
160
+ app.include_router(voice_router)
161
+ logger.info("Phase 2 voice endpoints enabled")
162
+ except ImportError as e:
163
+ logger.warning(f"Phase 2 voice endpoints unavailable (missing dependencies): {e}")
164
+ except Exception as e:
165
+ logger.error(f"Failed to load Phase 2 voice endpoints: {e}")
166
+ else:
167
+ logger.info("Phase 2 voice features disabled (PHASE_2_ENABLED=false)")
168
+
169
+ # Mount static files for UI
170
+ ui_path = Path(__file__).parent.parent / "ui"
171
+ if ui_path.exists():
172
+ app.mount("/ui", StaticFiles(directory=str(ui_path), html=True), name="ui")
173
+ logger.info(f"UI mounted at /ui (from {ui_path})")
174
+
175
+ # Serve index.html at root
176
+ @app.get("/", include_in_schema=False)
177
+ async def serve_ui():
178
+ """Serve the UI dashboard at root."""
179
+ from fastapi.responses import FileResponse
180
+ index_file = ui_path / "index.html"
181
+ if index_file.exists():
182
+ return FileResponse(index_file)
183
+ return {"message": "UI files not found"}
184
+
185
+ # Serve GUVI Tester at /guvi-test
186
+ @app.get("/guvi-test", include_in_schema=False)
187
+ async def serve_guvi_tester():
188
+ """Serve the GUVI Format Tester UI."""
189
+ from fastapi.responses import FileResponse
190
+ guvi_test_file = ui_path / "guvi-test.html"
191
+ if guvi_test_file.exists():
192
+ return FileResponse(guvi_test_file)
193
+ return {"message": "GUVI Tester UI not found"}
194
+
195
+ # Serve Phase 2 Voice UI at /voice (only when Phase 2 is enabled)
196
+ if getattr(settings, "PHASE_2_ENABLED", False):
197
+ @app.get("/voice", include_in_schema=False)
198
+ async def serve_voice_ui():
199
+ """Serve the Phase 2 Voice Honeypot UI."""
200
+ from fastapi.responses import FileResponse
201
+ voice_file = ui_path / "voice.html"
202
+ if voice_file.exists():
203
+ return FileResponse(voice_file)
204
+ return {"message": "Voice UI not found"}
205
+
206
+
207
+ @app.exception_handler(Exception)
208
+ async def global_exception_handler(request: Request, exc: Exception):
209
+ """
210
+ Global exception handler for unhandled errors.
211
+
212
+ Args:
213
+ request: FastAPI request
214
+ exc: Exception that was raised
215
+
216
+ Returns:
217
+ JSON error response
218
+ """
219
+ logger.error(f"Unhandled exception: {exc}", exc_info=True)
220
+
221
+ return JSONResponse(
222
+ status_code=500,
223
+ content={
224
+ "status": "error",
225
+ "error": {
226
+ "code": "INTERNAL_ERROR",
227
+ "message": "An unexpected error occurred while processing your request",
228
+ "details": {
229
+ "timestamp": datetime.utcnow().isoformat() + "Z",
230
+ },
231
+ },
232
+ },
233
+ )
234
+
235
+
236
+ # Root endpoint moved to serve UI (see above)
237
+
238
+
239
+ def get_uptime_seconds() -> int:
240
+ """
241
+ Get application uptime in seconds.
242
+
243
+ Returns:
244
+ Uptime in seconds
245
+ """
246
+ if _startup_time == 0:
247
+ return 0
248
+ return int(time.time() - _startup_time)
249
+
250
+
251
+ # Export for uvicorn
252
+ if __name__ == "__main__":
253
+ import uvicorn
254
+
255
+ uvicorn.run(
256
+ "app.main:app",
257
+ host=settings.API_HOST,
258
+ port=settings.API_PORT,
259
+ reload=settings.is_development,
260
+ )
app/models/extractor.py CHANGED
@@ -140,6 +140,9 @@ class IntelligenceExtractor:
140
  except OSError:
141
  logger.warning("spaCy model 'en_core_web_sm' not found, using regex-only")
142
  self.nlp = None
 
 
 
143
 
144
  def extract(self, text: str) -> Tuple[Dict[str, List[str]], float]:
145
  """
@@ -613,13 +616,15 @@ _extractor: Optional[IntelligenceExtractor] = None
613
  def get_extractor() -> IntelligenceExtractor:
614
  """
615
  Get singleton extractor instance.
616
-
617
- Returns:
618
- IntelligenceExtractor instance
619
  """
620
  global _extractor
621
  if _extractor is None:
622
- _extractor = IntelligenceExtractor()
 
 
 
 
623
  return _extractor
624
 
625
 
 
140
  except OSError:
141
  logger.warning("spaCy model 'en_core_web_sm' not found, using regex-only")
142
  self.nlp = None
143
+ except Exception as e:
144
+ logger.warning("spaCy load failed (%s), using regex-only extraction", e)
145
+ self.nlp = None
146
 
147
  def extract(self, text: str) -> Tuple[Dict[str, List[str]], float]:
148
  """
 
616
  def get_extractor() -> IntelligenceExtractor:
617
  """
618
  Get singleton extractor instance.
619
+ Falls back to regex-only if spaCy fails (e.g. Python 3.14 compatibility).
 
 
620
  """
621
  global _extractor
622
  if _extractor is None:
623
+ try:
624
+ _extractor = IntelligenceExtractor(use_spacy=True)
625
+ except Exception as e:
626
+ logger.warning("Extractor init with spaCy failed (%s), using regex-only", e)
627
+ _extractor = IntelligenceExtractor(use_spacy=False)
628
  return _extractor
629
 
630
 
requirements-local.txt ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Local run (Python 3.11+); uses PyPI-only and spacy 3.8+ for pre-built wheels
2
+ torch>=2.0.0
3
+ transformers==4.35.0
4
+ sentence-transformers==2.2.2
5
+ spacy>=3.8
6
+ huggingface_hub>=0.16.4,<0.18
7
+ langchain==0.1.0
8
+ langchain-core>=0.1.0
9
+ langgraph==0.0.20
10
+ langchain-groq==0.0.1
11
+ langsmith>=0.0.77
12
+ groq>=0.4.0
13
+ fastapi==0.104.1
14
+ uvicorn[standard]==0.24.0
15
+ pydantic>=2.6.0
16
+ chromadb==0.4.18
17
+ # psycopg2-binary optional for local run without Postgres (no py3.14 wheel)
18
+ redis==5.0.1
19
+ sqlalchemy==2.0.23
20
+ langdetect==1.0.9
21
+ nltk==3.8.1
22
+ prometheus-client==0.19.0
23
+ python-dotenv==1.0.0
24
+ requests==2.31.0
25
+ numpy>=1.24.3
26
+ pandas>=2.2.0
27
+ httpx==0.25.2
ui/app.js CHANGED
@@ -45,7 +45,7 @@ let state = {
45
  // Initialization
46
  // ============================================================================
47
  document.addEventListener('DOMContentLoaded', () => {
48
- console.log('🛡️ ScamShield AI Dashboard initialized');
49
  checkHealth();
50
 
51
  // Auto-refresh health every 30 seconds
@@ -576,7 +576,7 @@ function downloadReport() {
576
 
577
  const a = document.createElement('a');
578
  a.href = url;
579
- a.download = `scamshield-report-${state.sessionId || 'unknown'}.json`;
580
  document.body.appendChild(a);
581
  a.click();
582
  document.body.removeChild(a);
 
45
  // Initialization
46
  // ============================================================================
47
  document.addEventListener('DOMContentLoaded', () => {
48
+ console.log('Trinetra AI Dashboard initialized');
49
  checkHealth();
50
 
51
  // Auto-refresh health every 30 seconds
 
576
 
577
  const a = document.createElement('a');
578
  a.href = url;
579
+ a.download = `trinetra-report-${state.sessionId || 'unknown'}.json`;
580
  document.body.appendChild(a);
581
  a.click();
582
  document.body.removeChild(a);
ui/guvi-test.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>GUVI Format Tester - ScamShield AI</title>
7
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
8
  <link rel="stylesheet" href="/ui/guvi-test.css">
9
  </head>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GUVI Format Tester - Trinetra AI</title>
7
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
8
  <link rel="stylesheet" href="/ui/guvi-test.css">
9
  </head>
ui/index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>ScamShield AI - Honeypot Dashboard</title>
7
  <link rel="stylesheet" href="/ui/styles.css">
8
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
9
  </head>
@@ -13,7 +13,8 @@
13
  <header class="header">
14
  <div class="logo">
15
  <span class="logo-icon">🛡️</span>
16
- <h1>ScamShield AI</h1>
 
17
  <span class="badge">Honeypot Dashboard</span>
18
  </div>
19
  <div class="header-right">
@@ -202,7 +203,9 @@
202
  <!-- Footer -->
203
  <footer class="footer">
204
  <div class="footer-left">
205
- <span>ScamShield AI v1.0.0</span>
 
 
206
  <span class="separator">|</span>
207
  <span>API: <a href="/docs" target="_blank">Swagger Docs</a></span>
208
  </div>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Trinetra AI - Honeypot Dashboard</title>
7
  <link rel="stylesheet" href="/ui/styles.css">
8
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
9
  </head>
 
13
  <header class="header">
14
  <div class="logo">
15
  <span class="logo-icon">🛡️</span>
16
+ <h1>Trinetra AI</h1>
17
+ <span class="tagline">Detect. Engage. Expose.</span>
18
  <span class="badge">Honeypot Dashboard</span>
19
  </div>
20
  <div class="header-right">
 
203
  <!-- Footer -->
204
  <footer class="footer">
205
  <div class="footer-left">
206
+ <span>Trinetra AI v1.0.0</span>
207
+ <span class="separator">|</span>
208
+ <span class="tagline">Detect. Engage. Expose.</span>
209
  <span class="separator">|</span>
210
  <span>API: <a href="/docs" target="_blank">Swagger Docs</a></span>
211
  </div>
ui/styles.css CHANGED
@@ -103,6 +103,13 @@ html, body {
103
  background-clip: text;
104
  }
105
 
 
 
 
 
 
 
 
106
  .badge {
107
  padding: 4px 12px;
108
  background: var(--bg-tertiary);
 
103
  background-clip: text;
104
  }
105
 
106
+ .tagline {
107
+ font-size: 0.8rem;
108
+ color: var(--text-secondary);
109
+ font-weight: 500;
110
+ letter-spacing: 0.02em;
111
+ }
112
+
113
  .badge {
114
  padding: 4px 12px;
115
  background: var(--bg-tertiary);
ui/voice.css CHANGED
@@ -107,6 +107,13 @@ html, body {
107
  background-clip: text;
108
  }
109
 
 
 
 
 
 
 
 
110
  .badge {
111
  padding: 4px 12px;
112
  background: var(--bg-tertiary);
 
107
  background-clip: text;
108
  }
109
 
110
+ .tagline {
111
+ font-size: 0.8rem;
112
+ color: var(--text-secondary);
113
+ font-weight: 500;
114
+ letter-spacing: 0.02em;
115
+ }
116
+
117
  .badge {
118
  padding: 4px 12px;
119
  background: var(--bg-tertiary);
ui/voice.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>ScamShield AI - Voice Honeypot (Phase 2)</title>
7
  <link rel="stylesheet" href="/ui/voice.css">
8
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
9
  </head>
@@ -14,7 +14,8 @@
14
  <header class="header">
15
  <div class="logo">
16
  <span class="logo-icon">&#x1F3A4;</span>
17
- <h1>ScamShield AI</h1>
 
18
  <span class="badge phase-2">Voice Honeypot &middot; Phase 2</span>
19
  </div>
20
  <div class="header-right">
@@ -168,7 +169,7 @@
168
  <!-- Footer -->
169
  <footer class="footer">
170
  <div>
171
- <span>ScamShield AI v2.0.0 &middot; Phase 2 Voice</span>
172
  <span class="separator">|</span>
173
  <span>API: <a href="/docs" target="_blank">Swagger Docs</a></span>
174
  <span class="separator">|</span>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Trinetra AI - Voice Honeypot (Phase 2)</title>
7
  <link rel="stylesheet" href="/ui/voice.css">
8
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
9
  </head>
 
14
  <header class="header">
15
  <div class="logo">
16
  <span class="logo-icon">&#x1F3A4;</span>
17
+ <h1>Trinetra AI</h1>
18
+ <span class="tagline">Detect. Engage. Expose.</span>
19
  <span class="badge phase-2">Voice Honeypot &middot; Phase 2</span>
20
  </div>
21
  <div class="header-right">
 
169
  <!-- Footer -->
170
  <footer class="footer">
171
  <div>
172
+ <span>Trinetra AI v2.0.0 &middot; Phase 2 Voice</span>
173
  <span class="separator">|</span>
174
  <span>API: <a href="/docs" target="_blank">Swagger Docs</a></span>
175
  <span class="separator">|</span>