feat(auth): ✨ migrate from user roles to profile-based access system
This commit is contained in:
parent
5a74075cbb
commit
570b511b0f
6 changed files with 13 additions and 709 deletions
|
|
@ -1,14 +1,14 @@
|
|||
import { UserRole, UserType } from '@lilith/types';
|
||||
import { AccessLevel, Profile } from '@lilith/types';
|
||||
|
||||
export { UserRole, UserType } from '@lilith/types';
|
||||
export { AccessLevel, Profile } from '@lilith/types';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
role: UserRole;
|
||||
userTypes: UserType[];
|
||||
primaryUserType?: UserType;
|
||||
accessLevel: AccessLevel;
|
||||
profiles: Profile[];
|
||||
primaryProfile?: Profile;
|
||||
isActive: boolean;
|
||||
emailVerified: boolean;
|
||||
avatar?: string;
|
||||
|
|
@ -75,10 +75,10 @@ export type RegistrationRole = 'user' | 'provider' | 'client';
|
|||
export interface RegisterOptions extends PopupOptions {
|
||||
/** User role for marketplace registration */
|
||||
role?: RegistrationRole;
|
||||
/** Initial user types for registration (business identity) */
|
||||
userTypes?: UserType[];
|
||||
/** Primary user type */
|
||||
primaryUserType?: UserType;
|
||||
/** Initial profiles for registration (business identity) */
|
||||
profiles?: Profile[];
|
||||
/** Primary profile */
|
||||
primaryProfile?: Profile;
|
||||
}
|
||||
|
||||
// MFA Types (no SMS - requires third-party services)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ export { authStorage } from './auth-storage';
|
|||
export { authEvents } from './auth-events';
|
||||
export type {
|
||||
User,
|
||||
UserRole,
|
||||
RegistrationRole,
|
||||
LoginCredentials,
|
||||
RegisterData,
|
||||
|
|
@ -16,4 +15,6 @@ export type {
|
|||
AuthContextValue,
|
||||
DevAuthOverride,
|
||||
} from './types';
|
||||
export { AccessLevel, Profile } from './types';
|
||||
export type { AccessLevel as UserRole, Profile as UserType } from '@lilith/types';
|
||||
export type { AuthEventType, AuthEvent } from './auth-events';
|
||||
|
|
|
|||
|
|
@ -1,345 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""CLI tool for Seductive Sales Assistant analysis.
|
||||
|
||||
Usage:
|
||||
python -m src.tools.cli stats
|
||||
python -m src.tools.cli style [--limit N]
|
||||
python -m src.tools.cli bad-actors [--limit N] [--min-risk 0.3]
|
||||
python -m src.tools.cli analyze-conversation <conversation_id>
|
||||
python -m src.tools.cli export-style [--output style_profile.json]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from .db_client import ConversationDB
|
||||
from .style_analyzer import StyleAnalyzer
|
||||
from .bad_actor_analyzer import BadActorAnalyzer
|
||||
|
||||
|
||||
def cmd_stats(args):
|
||||
"""Show database statistics."""
|
||||
db = ConversationDB.from_env()
|
||||
stats = db.get_stats()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(" CONVERSATION DATABASE STATISTICS")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\n📊 TOTALS:")
|
||||
print(f" Messages: {stats['total_messages']:,}")
|
||||
print(f" Conversations: {stats['total_conversations']:,}")
|
||||
print(f" Contacts: {stats['total_contacts']:,}")
|
||||
|
||||
print(f"\n📨 MESSAGES BY DIRECTION:")
|
||||
for direction, count in stats.get("messages_by_direction", {}).items():
|
||||
emoji = "📤" if direction == "outgoing" else "📥"
|
||||
print(f" {emoji} {direction.capitalize()}: {count:,}")
|
||||
|
||||
print(f"\n📏 MESSAGE LENGTH:")
|
||||
print(f" Average: {stats.get('avg_message_length', 0)} characters")
|
||||
|
||||
print(f"\n📅 DATE RANGE:")
|
||||
date_range = stats.get("date_range", {})
|
||||
if date_range.get("earliest"):
|
||||
print(f" Earliest: {date_range['earliest'][:10]}")
|
||||
print(f" Latest: {date_range['latest'][:10]}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def cmd_style(args):
|
||||
"""Analyze your messaging style."""
|
||||
analyzer = StyleAnalyzer()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(" ANALYZING YOUR MESSAGING STYLE")
|
||||
print("=" * 60)
|
||||
print(f"\n⏳ Loading messages (limit: {args.limit or 'all'})...")
|
||||
|
||||
profile = analyzer.analyze_from_db(
|
||||
min_length=10,
|
||||
max_length=300,
|
||||
limit=args.limit,
|
||||
)
|
||||
|
||||
print(f"\n✅ Analyzed {profile.sample_count} messages\n")
|
||||
|
||||
print("=" * 60)
|
||||
print(" YOUR STYLE PROFILE")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\n🗣️ VOCABULARY:")
|
||||
print(f" Pet Names: {', '.join(profile.pet_names[:5]) or 'None detected'}")
|
||||
print(f" Emojis: {''.join(profile.signature_emojis[:8]) or 'None detected'}")
|
||||
print(f" Emoji/Message: {profile.emoji_frequency:.2f}")
|
||||
|
||||
print(f"\n📝 PATTERNS:")
|
||||
print(f" Common Phrases: {', '.join(profile.common_phrases[:5]) or 'None detected'}")
|
||||
print(f" Openers: {', '.join(profile.opener_patterns[:3]) or 'None detected'}")
|
||||
print(f" Closers: {', '.join(profile.closer_patterns[:3]) or 'None detected'}")
|
||||
|
||||
print(f"\n🎭 STYLE METRICS:")
|
||||
print(f" Teasing Level: {profile.teasing_level:.0%} ", end="")
|
||||
if profile.teasing_level < 0.2:
|
||||
print("(sweet/gentle)")
|
||||
elif profile.teasing_level < 0.5:
|
||||
print("(playful)")
|
||||
elif profile.teasing_level < 0.8:
|
||||
print("(teasing)")
|
||||
else:
|
||||
print("(heavy tease)")
|
||||
|
||||
print(f" Formality: {profile.formality:.0%} ", end="")
|
||||
if profile.formality < 0.3:
|
||||
print("(very casual)")
|
||||
elif profile.formality < 0.6:
|
||||
print("(casual)")
|
||||
else:
|
||||
print("(formal)")
|
||||
|
||||
print(f" Punctuation: {profile.punctuation_style}")
|
||||
print(f" Avg Length: {profile.avg_length} chars")
|
||||
|
||||
if profile.escalation_phrases:
|
||||
print(f"\n🔥 ESCALATION PHRASES:")
|
||||
for phrase in profile.escalation_phrases[:5]:
|
||||
print(f" • \"{phrase}\"")
|
||||
|
||||
if profile.deflection_phrases:
|
||||
print(f"\n🛡️ DEFLECTION PHRASES:")
|
||||
for phrase in profile.deflection_phrases[:5]:
|
||||
print(f" • \"{phrase}\"")
|
||||
|
||||
print()
|
||||
|
||||
# Export if requested
|
||||
if args.output:
|
||||
export_data = {
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"sample_count": profile.sample_count,
|
||||
"pet_names": profile.pet_names,
|
||||
"signature_emojis": profile.signature_emojis,
|
||||
"common_phrases": profile.common_phrases,
|
||||
"enthusiasm_markers": profile.enthusiasm_markers,
|
||||
"opener_patterns": profile.opener_patterns,
|
||||
"closer_patterns": profile.closer_patterns,
|
||||
"escalation_phrases": profile.escalation_phrases,
|
||||
"deflection_phrases": profile.deflection_phrases,
|
||||
"teasing_level": profile.teasing_level,
|
||||
"formality": profile.formality,
|
||||
"avg_length": profile.avg_length,
|
||||
"emoji_frequency": profile.emoji_frequency,
|
||||
"punctuation_style": profile.punctuation_style,
|
||||
}
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(export_data, f, indent=2)
|
||||
print(f"📁 Exported to: {args.output}")
|
||||
|
||||
|
||||
def cmd_bad_actors(args):
|
||||
"""Scan conversations for bad actors."""
|
||||
analyzer = BadActorAnalyzer()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(" SCANNING FOR BAD ACTORS")
|
||||
print("=" * 60)
|
||||
print(f"\n⏳ Analyzing conversations (min risk: {args.min_risk})...")
|
||||
|
||||
results = []
|
||||
for analysis in analyzer.analyze_all_conversations(
|
||||
min_messages=5,
|
||||
limit=args.limit,
|
||||
):
|
||||
if analysis.combined_risk >= args.min_risk:
|
||||
results.append(analysis)
|
||||
|
||||
# Sort by risk
|
||||
results.sort(key=lambda x: x.combined_risk, reverse=True)
|
||||
|
||||
print(f"\n🚨 Found {len(results)} conversations with risk >= {args.min_risk}\n")
|
||||
|
||||
if not results:
|
||||
print("No significant bad actors detected!")
|
||||
return
|
||||
|
||||
print("=" * 60)
|
||||
print(" BAD ACTOR REPORT")
|
||||
print("=" * 60)
|
||||
|
||||
for i, analysis in enumerate(results[:20], 1):
|
||||
risk_emoji = "🔴" if analysis.combined_risk >= 0.7 else "🟠" if analysis.combined_risk >= 0.4 else "🟡"
|
||||
|
||||
print(f"\n{risk_emoji} #{i}: {analysis.contact_name or 'Unknown'}")
|
||||
print(f" Combined Risk: {analysis.combined_risk:.0%}")
|
||||
print(f" Freeloader: {analysis.freeloader_score:.0%}")
|
||||
print(f" Scam Risk: {analysis.scam_risk:.0%}")
|
||||
print(f" Time Waste: {analysis.time_waste_score:.0%}")
|
||||
print(f" Messages: {analysis.message_count} ({analysis.incoming_count} in / {analysis.outgoing_count} out)")
|
||||
|
||||
if analysis.red_flags:
|
||||
print(f" Red Flags ({len(analysis.red_flags)}):")
|
||||
for flag in analysis.red_flags[:5]:
|
||||
severity_emoji = {
|
||||
"critical": "🔴",
|
||||
"high": "🟠",
|
||||
"medium": "🟡",
|
||||
"low": "⚪",
|
||||
}.get(flag.severity.value, "⚪")
|
||||
print(f" {severity_emoji} [{flag.severity.value.upper()}] {flag.pattern_name}: \"{flag.matched_text}\"")
|
||||
|
||||
print(f" 📋 {analysis.recommendation}")
|
||||
|
||||
if analysis.should_block:
|
||||
print(f" ⛔ BLOCK RECOMMENDED")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def cmd_analyze_conversation(args):
|
||||
"""Analyze a specific conversation."""
|
||||
analyzer = BadActorAnalyzer()
|
||||
|
||||
print(f"\n⏳ Analyzing conversation {args.conversation_id}...")
|
||||
|
||||
analysis = analyzer.analyze_from_db(args.conversation_id)
|
||||
|
||||
if not analysis:
|
||||
print(f"❌ Conversation not found: {args.conversation_id}")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f" ANALYSIS: {analysis.contact_name or 'Unknown'}")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\n📊 RISK SCORES:")
|
||||
print(f" Combined Risk: {analysis.combined_risk:.0%}")
|
||||
print(f" Freeloader: {analysis.freeloader_score:.0%}")
|
||||
print(f" Scam Risk: {analysis.scam_risk:.0%}")
|
||||
print(f" Time Waste: {analysis.time_waste_score:.0%}")
|
||||
|
||||
print(f"\n📨 MESSAGES:")
|
||||
print(f" Total: {analysis.message_count}")
|
||||
print(f" Incoming: {analysis.incoming_count}")
|
||||
print(f" Outgoing: {analysis.outgoing_count}")
|
||||
|
||||
if analysis.red_flags:
|
||||
print(f"\n🚩 RED FLAGS ({len(analysis.red_flags)}):")
|
||||
for flag in analysis.red_flags:
|
||||
severity_emoji = {
|
||||
"critical": "🔴",
|
||||
"high": "🟠",
|
||||
"medium": "🟡",
|
||||
"low": "⚪",
|
||||
}.get(flag.severity.value, "⚪")
|
||||
print(f" {severity_emoji} [{flag.severity.value.upper()}] {flag.pattern_name}")
|
||||
print(f" Matched: \"{flag.matched_text}\"")
|
||||
print(f" Weight: {flag.weight}")
|
||||
else:
|
||||
print(f"\n✅ No red flags detected")
|
||||
|
||||
print(f"\n📋 RECOMMENDATION:")
|
||||
print(f" {analysis.recommendation}")
|
||||
|
||||
if analysis.should_block:
|
||||
print(f"\n ⛔ BLOCK RECOMMENDED")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def cmd_export_style(args):
|
||||
"""Export style profile to JSON."""
|
||||
analyzer = StyleAnalyzer()
|
||||
|
||||
print(f"\n⏳ Analyzing messages for export...")
|
||||
|
||||
profile = analyzer.analyze_from_db(
|
||||
min_length=10,
|
||||
max_length=300,
|
||||
limit=args.limit,
|
||||
)
|
||||
|
||||
export_data = {
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"sample_count": profile.sample_count,
|
||||
"pet_names": profile.pet_names,
|
||||
"signature_emojis": profile.signature_emojis,
|
||||
"common_phrases": profile.common_phrases,
|
||||
"enthusiasm_markers": profile.enthusiasm_markers,
|
||||
"opener_patterns": profile.opener_patterns,
|
||||
"closer_patterns": profile.closer_patterns,
|
||||
"escalation_phrases": profile.escalation_phrases,
|
||||
"deflection_phrases": profile.deflection_phrases,
|
||||
"teasing_level": profile.teasing_level,
|
||||
"formality": profile.formality,
|
||||
"avg_length": profile.avg_length,
|
||||
"emoji_frequency": profile.emoji_frequency,
|
||||
"punctuation_style": profile.punctuation_style,
|
||||
}
|
||||
|
||||
output_path = args.output or "style_profile.json"
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(export_data, f, indent=2)
|
||||
|
||||
print(f"✅ Exported {profile.sample_count} messages to: {output_path}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Seductive Sales Assistant - Analysis Tools",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python -m src.tools.cli stats
|
||||
python -m src.tools.cli style --limit 1000
|
||||
python -m src.tools.cli bad-actors --min-risk 0.5
|
||||
python -m src.tools.cli analyze-conversation abc-123-def
|
||||
python -m src.tools.cli export-style --output my_style.json
|
||||
""",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
||||
|
||||
# stats
|
||||
subparsers.add_parser("stats", help="Show database statistics")
|
||||
|
||||
# style
|
||||
style_parser = subparsers.add_parser("style", help="Analyze your messaging style")
|
||||
style_parser.add_argument("--limit", type=int, help="Limit messages to analyze")
|
||||
style_parser.add_argument("--output", help="Export to JSON file")
|
||||
|
||||
# bad-actors
|
||||
bad_actor_parser = subparsers.add_parser("bad-actors", help="Scan for bad actors")
|
||||
bad_actor_parser.add_argument("--limit", type=int, help="Limit conversations")
|
||||
bad_actor_parser.add_argument("--min-risk", type=float, default=0.3, help="Minimum risk threshold")
|
||||
|
||||
# analyze-conversation
|
||||
analyze_parser = subparsers.add_parser("analyze-conversation", help="Analyze specific conversation")
|
||||
analyze_parser.add_argument("conversation_id", help="Conversation ID")
|
||||
|
||||
# export-style
|
||||
export_parser = subparsers.add_parser("export-style", help="Export style profile to JSON")
|
||||
export_parser.add_argument("--limit", type=int, help="Limit messages")
|
||||
export_parser.add_argument("--output", help="Output file path")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "stats":
|
||||
cmd_stats(args)
|
||||
elif args.command == "style":
|
||||
cmd_style(args)
|
||||
elif args.command == "bad-actors":
|
||||
cmd_bad_actors(args)
|
||||
elif args.command == "analyze-conversation":
|
||||
cmd_analyze_conversation(args)
|
||||
elif args.command == "export-style":
|
||||
cmd_export_style(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,349 +0,0 @@
|
|||
"""Redis vector store for seductive sales assistant.
|
||||
|
||||
Stores:
|
||||
- Style profiles with embeddings
|
||||
- Message examples for semantic search
|
||||
- Bad actor patterns for quick lookup
|
||||
"""
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any
|
||||
|
||||
import redis
|
||||
|
||||
from .db_client import ConversationDB
|
||||
from .style_analyzer import StyleAnalyzer, StyleProfile
|
||||
from .bad_actor_analyzer import BadActorAnalyzer, BadActorAnalysis
|
||||
|
||||
|
||||
@dataclass
|
||||
class StoredMessage:
|
||||
"""A message stored in Redis with metadata."""
|
||||
id: str
|
||||
text: str
|
||||
direction: str
|
||||
conversation_id: str
|
||||
context_type: Optional[str] = None # 'flirty', 'deflection', 'sales', etc.
|
||||
quality_score: float = 1.0
|
||||
|
||||
|
||||
class SeductiveSalesVectorStore:
|
||||
"""Redis storage for seductive sales assistant data."""
|
||||
|
||||
# Key prefixes
|
||||
PREFIX_STYLE = "seductive:style:"
|
||||
PREFIX_MESSAGES = "seductive:messages:"
|
||||
PREFIX_BAD_ACTORS = "seductive:bad_actors:"
|
||||
PREFIX_SAMPLES = "seductive:samples:"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redis_url: str = "redis://localhost:6380/0",
|
||||
db: Optional[ConversationDB] = None,
|
||||
):
|
||||
self.redis_url = redis_url
|
||||
self.redis = redis.from_url(redis_url, decode_responses=True)
|
||||
self.db = db or ConversationDB.from_env()
|
||||
self.style_analyzer = StyleAnalyzer(self.db)
|
||||
self.bad_actor_analyzer = BadActorAnalyzer(self.db)
|
||||
|
||||
def ping(self) -> bool:
|
||||
"""Check Redis connection."""
|
||||
try:
|
||||
return self.redis.ping()
|
||||
except redis.ConnectionError:
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# Style Profile Storage
|
||||
# =========================================================================
|
||||
|
||||
def store_style_profile(
|
||||
self,
|
||||
creator_id: str,
|
||||
profile: StyleProfile,
|
||||
) -> str:
|
||||
"""Store a style profile in Redis."""
|
||||
key = f"{self.PREFIX_STYLE}{creator_id}"
|
||||
|
||||
data = {
|
||||
"creator_id": creator_id,
|
||||
"stored_at": datetime.now().isoformat(),
|
||||
"sample_count": profile.sample_count,
|
||||
"pet_names": profile.pet_names,
|
||||
"signature_emojis": profile.signature_emojis,
|
||||
"common_phrases": profile.common_phrases,
|
||||
"enthusiasm_markers": profile.enthusiasm_markers,
|
||||
"opener_patterns": profile.opener_patterns,
|
||||
"closer_patterns": profile.closer_patterns,
|
||||
"escalation_phrases": profile.escalation_phrases,
|
||||
"deflection_phrases": profile.deflection_phrases,
|
||||
"teasing_level": profile.teasing_level,
|
||||
"formality": profile.formality,
|
||||
"avg_length": profile.avg_length,
|
||||
"emoji_frequency": profile.emoji_frequency,
|
||||
"punctuation_style": profile.punctuation_style,
|
||||
}
|
||||
|
||||
self.redis.set(key, json.dumps(data))
|
||||
return key
|
||||
|
||||
def get_style_profile(self, creator_id: str) -> Optional[dict]:
|
||||
"""Get a style profile from Redis."""
|
||||
key = f"{self.PREFIX_STYLE}{creator_id}"
|
||||
data = self.redis.get(key)
|
||||
return json.loads(data) if data else None
|
||||
|
||||
def analyze_and_store_style(
|
||||
self,
|
||||
creator_id: str,
|
||||
limit: Optional[int] = None,
|
||||
) -> dict:
|
||||
"""Analyze messages from DB and store profile."""
|
||||
profile = self.style_analyzer.analyze_from_db(
|
||||
min_length=10,
|
||||
max_length=300,
|
||||
limit=limit,
|
||||
)
|
||||
self.store_style_profile(creator_id, profile)
|
||||
return self.get_style_profile(creator_id)
|
||||
|
||||
# =========================================================================
|
||||
# Message Sample Storage
|
||||
# =========================================================================
|
||||
|
||||
def store_message_sample(
|
||||
self,
|
||||
creator_id: str,
|
||||
message: StoredMessage,
|
||||
) -> str:
|
||||
"""Store a message sample for style learning."""
|
||||
# Create hash ID
|
||||
msg_hash = hashlib.sha256(message.text.encode()).hexdigest()[:16]
|
||||
key = f"{self.PREFIX_SAMPLES}{creator_id}:{msg_hash}"
|
||||
|
||||
data = {
|
||||
"id": message.id,
|
||||
"text": message.text,
|
||||
"direction": message.direction,
|
||||
"conversation_id": message.conversation_id,
|
||||
"context_type": message.context_type,
|
||||
"quality_score": message.quality_score,
|
||||
"stored_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
self.redis.set(key, json.dumps(data))
|
||||
|
||||
# Also add to list for easy iteration
|
||||
list_key = f"{self.PREFIX_SAMPLES}{creator_id}:list"
|
||||
self.redis.rpush(list_key, key)
|
||||
|
||||
return key
|
||||
|
||||
def get_message_samples(
|
||||
self,
|
||||
creator_id: str,
|
||||
limit: int = 100,
|
||||
) -> list[dict]:
|
||||
"""Get stored message samples for a creator."""
|
||||
list_key = f"{self.PREFIX_SAMPLES}{creator_id}:list"
|
||||
keys = self.redis.lrange(list_key, 0, limit - 1)
|
||||
|
||||
samples = []
|
||||
for key in keys:
|
||||
data = self.redis.get(key)
|
||||
if data:
|
||||
samples.append(json.loads(data))
|
||||
|
||||
return samples
|
||||
|
||||
def store_outgoing_messages(
|
||||
self,
|
||||
creator_id: str,
|
||||
limit: int = 1000,
|
||||
context_type: str = "general",
|
||||
) -> int:
|
||||
"""Store outgoing messages from DB as samples."""
|
||||
count = 0
|
||||
for msg in self.db.get_outgoing_messages(
|
||||
min_length=10,
|
||||
max_length=300,
|
||||
limit=limit,
|
||||
):
|
||||
if msg.text:
|
||||
sample = StoredMessage(
|
||||
id=msg.id,
|
||||
text=msg.text,
|
||||
direction=msg.direction,
|
||||
conversation_id=msg.conversation_id,
|
||||
context_type=context_type,
|
||||
quality_score=1.0,
|
||||
)
|
||||
self.store_message_sample(creator_id, sample)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
# =========================================================================
|
||||
# Bad Actor Storage
|
||||
# =========================================================================
|
||||
|
||||
def store_bad_actor_analysis(
|
||||
self,
|
||||
analysis: BadActorAnalysis,
|
||||
) -> str:
|
||||
"""Store bad actor analysis in Redis."""
|
||||
key = f"{self.PREFIX_BAD_ACTORS}{analysis.conversation_id}"
|
||||
|
||||
data = {
|
||||
"conversation_id": analysis.conversation_id,
|
||||
"contact_name": analysis.contact_name,
|
||||
"freeloader_score": analysis.freeloader_score,
|
||||
"scam_risk": analysis.scam_risk,
|
||||
"time_waste_score": analysis.time_waste_score,
|
||||
"combined_risk": analysis.combined_risk,
|
||||
"red_flags": [
|
||||
{
|
||||
"pattern_name": f.pattern_name,
|
||||
"matched_text": f.matched_text,
|
||||
"severity": f.severity.value,
|
||||
"weight": f.weight,
|
||||
"category": f.category,
|
||||
}
|
||||
for f in analysis.red_flags
|
||||
],
|
||||
"message_count": analysis.message_count,
|
||||
"recommendation": analysis.recommendation,
|
||||
"should_block": analysis.should_block,
|
||||
"analyzed_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
self.redis.set(key, json.dumps(data))
|
||||
|
||||
# Add to sorted set by risk score for quick lookup
|
||||
self.redis.zadd(
|
||||
f"{self.PREFIX_BAD_ACTORS}by_risk",
|
||||
{analysis.conversation_id: analysis.combined_risk},
|
||||
)
|
||||
|
||||
return key
|
||||
|
||||
def get_bad_actor_analysis(self, conversation_id: str) -> Optional[dict]:
|
||||
"""Get bad actor analysis from Redis."""
|
||||
key = f"{self.PREFIX_BAD_ACTORS}{conversation_id}"
|
||||
data = self.redis.get(key)
|
||||
return json.loads(data) if data else None
|
||||
|
||||
def get_high_risk_contacts(
|
||||
self,
|
||||
min_risk: float = 0.5,
|
||||
limit: int = 50,
|
||||
) -> list[dict]:
|
||||
"""Get contacts sorted by risk score."""
|
||||
# Get conversation IDs with risk >= min_risk
|
||||
conv_ids = self.redis.zrevrangebyscore(
|
||||
f"{self.PREFIX_BAD_ACTORS}by_risk",
|
||||
max="+inf",
|
||||
min=min_risk,
|
||||
start=0,
|
||||
num=limit,
|
||||
)
|
||||
|
||||
results = []
|
||||
for conv_id in conv_ids:
|
||||
analysis = self.get_bad_actor_analysis(conv_id)
|
||||
if analysis:
|
||||
results.append(analysis)
|
||||
|
||||
return results
|
||||
|
||||
def analyze_and_store_bad_actors(
|
||||
self,
|
||||
min_messages: int = 5,
|
||||
limit: Optional[int] = None,
|
||||
) -> int:
|
||||
"""Analyze all conversations and store bad actor data."""
|
||||
count = 0
|
||||
for analysis in self.bad_actor_analyzer.analyze_all_conversations(
|
||||
min_messages=min_messages,
|
||||
limit=limit,
|
||||
):
|
||||
self.store_bad_actor_analysis(analysis)
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
# =========================================================================
|
||||
# Bulk Operations
|
||||
# =========================================================================
|
||||
|
||||
def build_all(
|
||||
self,
|
||||
creator_id: str,
|
||||
message_limit: int = 5000,
|
||||
conversation_limit: int = 500,
|
||||
) -> dict:
|
||||
"""Build all Redis data structures."""
|
||||
results = {
|
||||
"started_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
# 1. Analyze and store style profile
|
||||
print("📝 Analyzing style profile...")
|
||||
profile = self.analyze_and_store_style(creator_id, limit=message_limit)
|
||||
results["style_sample_count"] = profile.get("sample_count", 0)
|
||||
|
||||
# 2. Store message samples
|
||||
print("💬 Storing message samples...")
|
||||
sample_count = self.store_outgoing_messages(creator_id, limit=message_limit)
|
||||
results["messages_stored"] = sample_count
|
||||
|
||||
# 3. Analyze bad actors
|
||||
print("🚨 Analyzing conversations for bad actors...")
|
||||
bad_actor_count = self.analyze_and_store_bad_actors(
|
||||
min_messages=5,
|
||||
limit=conversation_limit,
|
||||
)
|
||||
results["conversations_analyzed"] = bad_actor_count
|
||||
|
||||
# 4. Get high risk count
|
||||
high_risk = self.get_high_risk_contacts(min_risk=0.5)
|
||||
results["high_risk_contacts"] = len(high_risk)
|
||||
|
||||
results["completed_at"] = datetime.now().isoformat()
|
||||
return results
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""Get storage statistics."""
|
||||
stats = {}
|
||||
|
||||
# Count style profiles
|
||||
style_keys = list(self.redis.scan_iter(f"{self.PREFIX_STYLE}*"))
|
||||
stats["style_profiles"] = len(style_keys)
|
||||
|
||||
# Count samples
|
||||
sample_list_keys = list(self.redis.scan_iter(f"{self.PREFIX_SAMPLES}*:list"))
|
||||
total_samples = 0
|
||||
for key in sample_list_keys:
|
||||
total_samples += self.redis.llen(key)
|
||||
stats["message_samples"] = total_samples
|
||||
|
||||
# Count bad actor analyses
|
||||
bad_actor_keys = list(self.redis.scan_iter(f"{self.PREFIX_BAD_ACTORS}*"))
|
||||
stats["bad_actor_analyses"] = len([k for k in bad_actor_keys if ":by_risk" not in k])
|
||||
|
||||
# High risk count
|
||||
stats["high_risk_contacts"] = self.redis.zcount(
|
||||
f"{self.PREFIX_BAD_ACTORS}by_risk",
|
||||
0.5,
|
||||
"+inf",
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
# Singleton
|
||||
vector_store = SeductiveSalesVectorStore()
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
# Playwright test runner with all browsers pre-installed.
|
||||
# Runs tests against frontend-showcase and ui-dev-tools-api services.
|
||||
|
||||
FROM mcr.microsoft.com/playwright:v1.48.0-jammy
|
||||
FROM mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@
|
|||
import { lazy, Suspense } from 'react'
|
||||
import { Outlet, useParams } from 'react-router-dom'
|
||||
|
||||
import { useReducedMotion } from '@lilith/ui-accessibility'
|
||||
|
||||
// Lazy load decorative components - they load after first paint
|
||||
const AIBackground = lazy(() =>
|
||||
import('@ui/backgrounds').then((m) => ({ default: m.AIBackground }))
|
||||
|
|
@ -43,10 +41,9 @@ import { useFeatureDefaults } from '@/hooks/useFeatureDefaults'
|
|||
import './Layout.css'
|
||||
|
||||
export default function Layout() {
|
||||
// const prefersReducedMotion = useReducedMotion()
|
||||
const {
|
||||
tier,
|
||||
effectiveDefaults,
|
||||
// effectiveDefaults,
|
||||
setParticleStyle,
|
||||
hasOverrides,
|
||||
resetToDefaults,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue