package api import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "regexp" "strings" "sync" "time" ) // ChatRequest is the incoming chat message from the client. type ChatRequest struct { SessionID string `json:"session_id"` Message string `json:"message"` History []Message `json:"history"` } // Message represents a chat message. type Message struct { Role string `json:"role"` Content string `json:"content"` } // ChatResponse is returned to the client. type ChatResponse struct { Reply string `json:"reply"` SessionID string `json:"session_id"` LeadCaptured bool `json:"lead_captured,omitempty"` } // Lead represents a captured lead. type Lead struct { Email string `json:"email"` SessionID string `json:"session_id"` Timestamp string `json:"timestamp"` Context string `json:"context"` } // Rate limiter for chat endpoint (20 requests/IP/hour) var ( chatRateMu sync.Mutex chatRateMap = make(map[string]*chatRateEntry) emailRegex = regexp.MustCompile(`[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`) leadsMu sync.Mutex ) type chatRateEntry struct { windowStart time.Time count int } // Aria's system prompt with embedded knowledge const ariaSystemPrompt = `You are Aria, the Dealspace product assistant. Dealspace is an M&A deal workflow platform for investment banks and advisors. Answer ONLY questions about Dealspace — its features, pricing, security, onboarding, use cases, and how it compares to alternatives like email-based data rooms or SharePoint. If asked anything outside Dealspace (personal advice, coding help, current events, competitor products, etc.), respond: "That's outside my expertise, but I'd love to connect you with our team. What's your email address?" If a user provides an email, respond: "Thanks! Someone from the Dealspace team will reach out to you shortly." Then stop. Be concise. 2-3 sentences max unless a detailed answer is genuinely needed. You are on a marketing website — your goal is to inform and convert, not to write essays. --- DEALSPACE KNOWLEDGE --- OVERVIEW: Dealspace is an M&A workflow platform trusted by investment banks. It's request-centric, secure, and intelligently simple. Unlike traditional VDRs that are document-centric (500-folder hierarchies nobody can navigate), Dealspace makes the Request the unit of work. CORE FEATURES: - Request-Centric Workflow: Every question, every answer, every status update is tracked, routed, and resolved. Structured request lists, status at a glance (open, assigned, answered, vetted, published), threaded communication. - Role-Based Simplicity: Your accountant sees their 3 tasks. Your CFO sees the big picture. Workstream-based access (Finance sees Finance, Legal sees Legal). Task inbox for contributors. - AI Matching: When a buyer submits a question, AI searches for existing answers. Human confirms, answer broadcasts to everyone who asked. Semantic search understands "revenue breakdown" and "sales by segment" are the same. Human in the loop - AI suggests, human confirms. Zero retention - deal data never trains AI models. - Work Where You Are: Email, Slack, Teams integration. No login required for basic responses. - Complete Audit Trail: Every access, every download, every routing hop logged. Access logs, download tracking, workflow history. HOW IT WORKS: 1. IB Creates Request List - Configure workstreams, invite participants, issue structured requests 2. Seller Responds - Internal routing, upload documents, mark complete 3. IB Vets & Approves - Quality control, approve to publish or reject with feedback 4. Buyers Access Data Room - Submit questions, AI matches to existing answers SECURITY: - SOC 2 Type II certified - FIPS 140-3 validated encryption (AES-256-GCM) - Per-deal encryption keys - one deal's compromise doesn't affect others - Encryption at rest and in transit (TLS 1.3) - ISO 27001 certified ISMS - GDPR compliant - Dynamic watermarking - every document watermarked with viewer identity, timestamp, and deal ID at serve time - SSO/SAML support, MFA required, IP allowlisting, session management - 99.99% uptime SLA, 24/7 security monitoring, <15min incident response PRICING: - Starter: $2,500/month - 1 concurrent deal, up to 10 participants, 10GB storage, request workflow, dynamic watermarking, audit trail, email support. No AI matching or SSO. - Professional (Most Popular): $7,500/month - 5 concurrent deals, unlimited participants, 100GB storage, everything in Starter plus AI matching and priority support. - Enterprise: Custom pricing - Unlimited deals, unlimited participants, unlimited storage, everything in Professional plus SSO/SAML, custom watermarks, dedicated support, 99.99% SLA, on-premise option. - Additional storage: $0.10/GB/month (no markups, actual cost) - 14-day free trial with full Professional features, no credit card required - Annual billing: 15% discount COMPARISON (50GB deal, 100 participants): - Dealspace Professional: $7,500/month total - Competitor A: $25,500/month ($5k base + $15k storage + $2.5k participants + $3k AI) - Competitor B: $17,500/month ($8k base + $8k storage + $1.5k participants) FAQ: - "Concurrent deal" = active deal not archived. Archived deals don't count toward limit. - Free trial: 14 days, full Professional features, no credit card - Storage overage: $0.10/GB, notified before billing - Upgrades prorated immediately, downgrades at next billing cycle COMPANY: - Operated by Muskepo B.V., Amsterdam - Offices: Amsterdam, New York, London - Contact: sales@dealspace.io, security@dealspace.io` // ChatHandler handles POST /api/chat requests func (h *Handlers) ChatHandler(w http.ResponseWriter, r *http.Request) { // CORS for muskepo.com origin := r.Header.Get("Origin") if origin == "https://muskepo.com" || origin == "http://localhost:8080" || strings.HasPrefix(origin, "http://82.24.174.112") { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") } if r.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent) return } // Rate limiting: 20 requests/IP/hour ip := realIP(r) if !checkChatRateLimit(ip) { ErrorResponse(w, http.StatusTooManyRequests, "rate_limited", "Too many requests. Please try again later.") return } // Parse request var req ChatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") return } if req.Message == "" { ErrorResponse(w, http.StatusBadRequest, "missing_message", "Message is required") return } // Limit history to last 6 messages if len(req.History) > 6 { req.History = req.History[len(req.History)-6:] } // Check for email in user message leadCaptured := false if emailRegex.MatchString(req.Message) { email := emailRegex.FindString(req.Message) if err := saveLead(email, req.SessionID, req.Message); err != nil { log.Printf("Failed to save lead: %v", err) } else { leadCaptured = true } } // Call Anthropic API reply, err := callAnthropicAPI(req.Message, req.History) if err != nil { log.Printf("Anthropic API error: %v", err) ErrorResponse(w, http.StatusInternalServerError, "api_error", "Sorry, I'm having trouble responding right now. Please try again.") return } // Check if assistant reply asks for email and user already provided one if leadCaptured && strings.Contains(strings.ToLower(reply), "email") { reply = "Thanks! Someone from the Dealspace team will reach out to you shortly." } JSONResponse(w, http.StatusOK, ChatResponse{ Reply: reply, SessionID: req.SessionID, LeadCaptured: leadCaptured, }) } func checkChatRateLimit(ip string) bool { chatRateMu.Lock() defer chatRateMu.Unlock() now := time.Now() entry, exists := chatRateMap[ip] // Clean up old entries for k, v := range chatRateMap { if now.Sub(v.windowStart) > time.Hour { delete(chatRateMap, k) } } if !exists || now.Sub(entry.windowStart) > time.Hour { chatRateMap[ip] = &chatRateEntry{windowStart: now, count: 1} return true } entry.count++ return entry.count <= 20 } func saveLead(email, sessionID, context string) error { leadsMu.Lock() defer leadsMu.Unlock() lead := Lead{ Email: email, SessionID: sessionID, Timestamp: time.Now().UTC().Format(time.RFC3339), Context: context, } // Ensure directory exists if err := os.MkdirAll("/opt/dealspace/data", 0755); err != nil { return fmt.Errorf("create data dir: %w", err) } f, err := os.OpenFile("/opt/dealspace/data/leads.jsonl", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("open leads file: %w", err) } defer f.Close() data, err := json.Marshal(lead) if err != nil { return fmt.Errorf("marshal lead: %w", err) } if _, err := f.Write(append(data, '\n')); err != nil { return fmt.Errorf("write lead: %w", err) } log.Printf("Lead captured: %s (session: %s)", email, sessionID) return nil } // AnthropicRequest is the request body for Anthropic API type AnthropicRequest struct { Model string `json:"model"` MaxTokens int `json:"max_tokens"` System string `json:"system"` Messages []AnthropicMessage `json:"messages"` } // AnthropicMessage represents a message in Anthropic format type AnthropicMessage struct { Role string `json:"role"` Content string `json:"content"` } // AnthropicResponse is the response from Anthropic API type AnthropicResponse struct { Content []struct { Text string `json:"text"` } `json:"content"` Error *struct { Message string `json:"message"` } `json:"error,omitempty"` } func callAnthropicAPI(userMessage string, history []Message) (string, error) { apiKey := os.Getenv("ANTHROPIC_API_KEY") if apiKey == "" { return "", fmt.Errorf("ANTHROPIC_API_KEY not set") } // Build messages array messages := make([]AnthropicMessage, 0, len(history)+1) for _, m := range history { messages = append(messages, AnthropicMessage{ Role: m.Role, Content: m.Content, }) } messages = append(messages, AnthropicMessage{ Role: "user", Content: userMessage, }) reqBody := AnthropicRequest{ Model: "claude-3-5-haiku-latest", MaxTokens: 300, System: ariaSystemPrompt, Messages: messages, } jsonData, err := json.Marshal(reqBody) if err != nil { return "", fmt.Errorf("marshal request: %w", err) } req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewBuffer(jsonData)) if err != nil { return "", fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("x-api-key", apiKey) req.Header.Set("anthropic-version", "2023-06-01") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("API request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("read response: %w", err) } if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("API error %d: %s", resp.StatusCode, string(body)) } var apiResp AnthropicResponse if err := json.Unmarshal(body, &apiResp); err != nil { return "", fmt.Errorf("unmarshal response: %w", err) } if apiResp.Error != nil { return "", fmt.Errorf("API error: %s", apiResp.Error.Message) } if len(apiResp.Content) == 0 { return "", fmt.Errorf("empty response from API") } return apiResp.Content[0].Text, nil }