From ba323cce2a621832016f90cdb317545a81f8b228 Mon Sep 17 00:00:00 2001 From: Sathvik Date: Wed, 23 Apr 2025 19:32:05 +0530 Subject: [PATCH] linked the backend with the project page search bar and chat page --- .gitignore | 3 + .../internal/handlers/chat_handler.go | 285 ++++++++++++++++++ .../internal/handlers/project_handler.go | 2 +- .../internal/handlers/search_handler.go | 254 ++++++++++++++++ .../internal/services/query_service.go | 35 ++- .../internal/utils/topic_search_prompt.go | 162 +++++++--- Loop_backend/main.go | 4 + Loop_frontend/app/chat/actions.ts | 37 +-- Loop_frontend/app/chat/page.tsx | 8 +- Loop_frontend/package.json | 1 + Loop_frontend/utils/api.ts | 42 +++ scripts/convert_project_content.py | 5 +- scripts/create_projects_script.py | 11 +- 13 files changed, 764 insertions(+), 85 deletions(-) create mode 100644 Loop_backend/internal/handlers/chat_handler.go diff --git a/.gitignore b/.gitignore index f562013..946ef57 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ **/*.env *.env /converted_dataset +/converted_dataset_temp +/converted_dataset_temp_v2 /dataset +/scripts/add_test_projects.py \ No newline at end of file diff --git a/Loop_backend/internal/handlers/chat_handler.go b/Loop_backend/internal/handlers/chat_handler.go new file mode 100644 index 0000000..60321b6 --- /dev/null +++ b/Loop_backend/internal/handlers/chat_handler.go @@ -0,0 +1,285 @@ +package handlers + +import ( + "Loop_backend/internal/ai/interfaces" + "Loop_backend/internal/response" + "Loop_backend/internal/services" + "encoding/json" + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "github.com/google/uuid" +) + +// ChatHandler handles chat-related requests +type ChatHandler struct { + searchService services.QueryService + summaryService services.SummaryService + provider interfaces.Provider // Your LLM provider +} + +// NewChatHandler creates a new chat handler +func NewChatHandler( + searchService services.QueryService, + summaryService services.SummaryService, + provider interfaces.Provider, +) *ChatHandler { + return &ChatHandler{ + searchService: searchService, + summaryService: summaryService, + provider: provider, + } +} + +// RegisterRoutes registers all routes for the chat handler +func (h *ChatHandler) RegisterRoutes(r RouteRegister) { + r.RegisterProtectedRoute("/api/chat/conversation", h.HandleConversation, nil) + r.RegisterProtectedRoute("/api/chat/history", h.GetChatHistory, nil) +} + +type ChatRequest struct { + Message string `json:"message"` +} + +type ChatResponse struct { + ID string `json:"id"` + Content string `json:"content"` + Type string `json:"type"` + Timestamp string `json:"timestamp"` +} + +// HandleConversation processes chat messages and routes them to the appropriate service +func (h *ChatHandler) HandleConversation(w http.ResponseWriter, r *http.Request) { + fmt.Println("[ChatHandler] HandleConversation: Request received") + + // Parse request + var chatReq ChatRequest + if err := json.NewDecoder(r.Body).Decode(&chatReq); err != nil { + fmt.Printf("[ChatHandler] ERROR: Failed to decode request: %v\n", err) + response.RespondWithError(w, http.StatusBadRequest, "Invalid request format") + return + } + + fmt.Printf("[ChatHandler] Message received: %s\n", chatReq.Message) + + // Analyze the message to determine intent + message := chatReq.Message + messageContent := strings.ToLower(message) + + fmt.Println("[ChatHandler] Analyzing message intent...") + + // Create response object + chatResp := ChatResponse{ + ID: uuid.New().String(), + Type: "llm", + Timestamp: time.Now().Format(time.RFC3339), + } + + fmt.Printf("[ChatHandler] Provider type: %T\n", h.provider) + + // Check for project summary intent + if matchesSummaryIntent(messageContent) { + fmt.Println("[ChatHandler] INTENT DETECTED: Summary request") + + projectName := extractProjectName(messageContent) + fmt.Printf("[ChatHandler] Extracted project name: %s\n", projectName) + + if projectName != "" { + // Search for the project first + fmt.Printf("[ChatHandler] Searching for project: %s\n", projectName) + searchResults, err := h.searchService.ExecuteSearchQuery(projectName) + + if err != nil { + fmt.Printf("[ChatHandler] ERROR: Search failed: %v\n", err) + chatResp.Content = "I couldn't find a project with that name. Please check the project name and try again." + } else if len(searchResults) == 0 { + fmt.Println("[ChatHandler] No matching projects found") + chatResp.Content = "I couldn't find a project with that name. Please check the project name and try again." + } else { + fmt.Printf("[ChatHandler] Found %d matching projects\n", len(searchResults)) + + // Get project ID from first result + projectID, ok := searchResults[0]["projectId"].(string) + if !ok || projectID == "" { + fmt.Println("[ChatHandler] ERROR: Project ID missing in search result") + chatResp.Content = "I found the project but couldn't generate a summary because of a data issue." + } else { + // Generate summary + fmt.Printf("[ChatHandler] Generating summary for project %s\n", projectID) + summary, err := h.summaryService.GenerateProjectSummary(projectID) + + if err != nil { + fmt.Printf("[ChatHandler] ERROR: Summary generation failed: %v\n", err) + chatResp.Content = "I found the project but couldn't generate a summary. Please try again later." + } else { + fmt.Println("[ChatHandler] Summary generated successfully") + + // Extract summary content + if summaryContent, ok := summary["summary"].(string); ok { + chatResp.Content = summaryContent + } else { + chatResp.Content = fmt.Sprintf("Here's what I found about the project: %s", searchResults[0]["projectName"].(string)) + } + } + } + } + } else { + fmt.Println("[ChatHandler] No project name could be extracted") + chatResp.Content = "Please specify which project you'd like a summary for." + } + } else if strings.Contains(messageContent, "search") || + strings.Contains(messageContent, "find") || + strings.Contains(messageContent, "projects about") || + strings.Contains(messageContent, "projects on") { + fmt.Println("[ChatHandler] INTENT DETECTED: Search query") + + // Handle search intent - Use topic search for better semantic understanding + query := extractSearchQuery(messageContent) + fmt.Printf("[ChatHandler] Extracted search query: %s\n", query) + + fmt.Println("[ChatHandler] Executing topic search query") + results, err := h.searchService.ExecuteTopicSearchQuery(query) + + if err != nil { + fmt.Printf("[ChatHandler] ERROR: Topic search failed: %v\n", err) + chatResp.Content = "I couldn't find any projects matching your query. Try a different search term." + } else if len(results) == 0 { + fmt.Println("[ChatHandler] No search results found") + chatResp.Content = "I couldn't find any projects matching your query. Try a different search term." + } else { + fmt.Printf("[ChatHandler] Found %d search results\n", len(results)) + + // Format search results for chat + var responseText strings.Builder + responseText.WriteString(fmt.Sprintf("I found %d projects matching your query:\n\n", len(results))) + + resultLimit := 5 + if len(results) < resultLimit { + resultLimit = len(results) + } + + for i := 0; i < resultLimit; i++ { + projectName, _ := results[i]["projectName"].(string) + projectDesc, _ := results[i]["description"].(string) + if len(projectDesc) > 100 { + projectDesc = projectDesc[:100] + "..." + } + + responseText.WriteString(fmt.Sprintf("**%s**\n%s\n\n", projectName, projectDesc)) + } + + if len(results) > resultLimit { + responseText.WriteString("... and more results.") + } + + chatResp.Content = responseText.String() + } + } else { + fmt.Println("[ChatHandler] INTENT DETECTED: General question - Using LLM") + + // For general questions, use the LLM directly + fmt.Printf("[ChatHandler] Sending to LLM provider: %s\n", message) + llmResponse, err := h.provider.Chat([]interfaces.Message{ + {Role: "user", Content: message}, + }) + + if err != nil { + fmt.Printf("[ChatHandler] ERROR: LLM request failed: %v\n", err) + chatResp.Content = "I'm sorry, I couldn't process your request right now." + } else { + fmt.Println("[ChatHandler] LLM response received successfully") + chatResp.Content = llmResponse.Content + } + } + + fmt.Println("[ChatHandler] Sending response to client") + response.RespondWithJSON(w, http.StatusOK, chatResp) +} + +// Extract search query from message +func extractSearchQuery(message string) string { + message = strings.ToLower(message) + message = strings.TrimPrefix(message, "search ") + message = strings.TrimPrefix(message, "find ") + message = strings.TrimPrefix(message, "projects about ") + message = strings.TrimPrefix(message, "projects on ") + return message +} + +// Detect if the message is requesting a project summary +func matchesSummaryIntent(message string) bool { + summaryTerms := []string{"summary", "summarize", "tell me about", "describe", "explain", "summarise"} + projectTerms := []string{"project", "app", "application", "system", "platform"} + + hasSummaryTerm := false + for _, term := range summaryTerms { + if strings.Contains(message, term) { + hasSummaryTerm = true + break + } + } + + hasProjectTerm := false + for _, term := range projectTerms { + if strings.Contains(message, term) { + hasProjectTerm = true + break + } + } + + return hasSummaryTerm && hasProjectTerm +} + +// Extract potential project name from a message +func extractProjectName(message string) string { + // First try common patterns + patterns := []string{ + `(?i)(?:summary|summarize|about|describe|explain).*?(?:project|app|system|platform)\s+(?:called|named)?\s*["']?([^"'.?!]+)["']?`, + `(?i)(?:summary|summarize|about|describe|explain)\s+(?:the|a)?\s*["']?([^"'.?!]+)["']?(?:\s+project|\s+app|\s+system|\s+platform)`, + } + + for _, pattern := range patterns { + re := regexp.MustCompile(pattern) + matches := re.FindStringSubmatch(message) + if len(matches) > 1 { + return strings.TrimSpace(matches[1]) + } + } + + // If no match with patterns, try extracting with keyword removal + words := strings.Fields(message) + filteredWords := make([]string, 0) + + // Remove common question words and filler words + stopWords := map[string]bool{ + "summary": true, "summarize": true, "tell": true, "me": true, "about": true, + "describe": true, "explain": true, "project": true, "app": true, "application": true, + "the": true, "a": true, "an": true, "for": true, "of": true, "to": true, + "give": true, "provide": true, "i": true, "want": true, "need": true, + "would": true, "like": true, "get": true, "called": true, "named": true, + } + + for _, word := range words { + w := strings.ToLower(word) + if !stopWords[w] { + filteredWords = append(filteredWords, word) + } + } + + // Use the remaining words as potential project name + if len(filteredWords) > 0 { + return strings.Join(filteredWords, " ") + } + + return "" +} + +// GetChatHistory retrieves the chat history for a user +func (h *ChatHandler) GetChatHistory(w http.ResponseWriter, r *http.Request) { + // This would typically retrieve chat history from a database + // For now, return an empty array + response.RespondWithJSON(w, http.StatusOK, []ChatResponse{}) +} diff --git a/Loop_backend/internal/handlers/project_handler.go b/Loop_backend/internal/handlers/project_handler.go index 72a2e30..fa6ac63 100644 --- a/Loop_backend/internal/handlers/project_handler.go +++ b/Loop_backend/internal/handlers/project_handler.go @@ -24,7 +24,7 @@ func NewProjectHandler(projectService services.ProjectService) *ProjectHandler { func (h *ProjectHandler) RegisterRoutes(r RouteRegister) { r.RegisterProtectedRoute("/api/project/create", h.CreateProject, &dto.CreateProjectRequest{}) - r.RegisterProtectedRoute("/api/project/search", h.SearchProjects, nil) + //r.RegisterProtectedRoute("/api/project/search", h.SearchProjects, nil) r.RegisterProtectedRoute("/api/project/{project_id:[a-fA-F0-9-]+}", h.GetProjectInfo, nil) r.RegisterProtectedRoute("/api/project/{project_id:[a-fA-F0-9-]+}/delete", h.DeleteProject, nil) } diff --git a/Loop_backend/internal/handlers/search_handler.go b/Loop_backend/internal/handlers/search_handler.go index d427da2..84885e2 100644 --- a/Loop_backend/internal/handlers/search_handler.go +++ b/Loop_backend/internal/handlers/search_handler.go @@ -7,6 +7,7 @@ import ( "Loop_backend/internal/services" "fmt" "net/http" + "strings" ) // SearchHandler handles search-related requests @@ -30,6 +31,7 @@ func NewSearchHandler( func (h *SearchHandler) RegisterRoutes(r RouteRegister) { r.RegisterProtectedRoute("/api/search", h.HandleSearch, &dto.SearchRequest{}) r.RegisterProtectedRoute("/api/topic-search", h.HandleTopicSearch, &dto.SearchRequest{}) + r.RegisterProtectedRoute("/api/project/search", h.HandleProjectSearch, nil) // Using query params } // HandleSearch converts natural language to cypher query and returns results @@ -114,3 +116,255 @@ func (h *SearchHandler) HandleTopicSearch(w http.ResponseWriter, r *http.Request "projects": enrichedResults, }) } + +// HandleProjectSearch handles unified search requests from the frontend +func (h *SearchHandler) HandleProjectSearch(w http.ResponseWriter, r *http.Request) { + // Extract keyword from query parameter + keyword := r.URL.Query().Get("keyword") + // Support mode parameter for future frontend toggle + mode := r.URL.Query().Get("mode") + + var results []map[string]interface{} + var err error + + if keyword == "" { + // Fetch all projects when no keyword is provided + fmt.Println("No keyword provided, fetching all projects") + results, err = h.queryService.GetAllProjects() + if err != nil { + response.RespondWithError(w, http.StatusInternalServerError, "Failed to fetch projects: "+err.Error()) + return + } + } else { + // Determine search type based on mode parameter or auto-detection + var useTopicSearch bool + + if mode == "advanced" { + // Frontend explicitly requested advanced search + useTopicSearch = true + } else if mode == "basic" { + // Frontend explicitly requested basic search + useTopicSearch = false + } else { + // Auto-detect based on query pattern (hybrid approach) + fmt.Println("Auto-detecting search type for query:", keyword) + useTopicSearch = shouldUseTopicSearch(keyword) + } + + if useTopicSearch { + fmt.Printf("Using topic search for query: %s\n", keyword) + results, err = h.queryService.ExecuteTopicSearchQuery(keyword) + } else { + fmt.Printf("Using regular search for query: %s\n", keyword) + results, err = h.queryService.ExecuteSearchQuery(keyword) + } + + if err != nil { + response.RespondWithError(w, http.StatusInternalServerError, "Failed to execute search: "+err.Error()) + return + } + } + + // Format results for frontend + projects := formatProjectResults(results) + + // Enrich projects with summaries + enrichedProjects := make([]map[string]interface{}, 0, len(projects)) + for _, project := range projects { + // Get the project ID + projectID, ok := project["id"].(string) + if !ok || projectID == "" { + // If no valid ID, just add the project without summary + enrichedProjects = append(enrichedProjects, project) + continue + } + + // Generate summary + summary, err := h.summaryService.GenerateProjectSummary(projectID) + if err != nil { + fmt.Printf("Error generating summary for project %s: %v\n", projectID, err) + // Add project without summary if generation fails + enrichedProjects = append(enrichedProjects, project) + continue + } + + // If summary contains a 'summary' field, extract it, otherwise use whole summary + var summaryText interface{} + if summaryContent, ok := summary["summary"]; ok { + summaryText = summaryContent + } else { + summaryText = summary + } + + // Add summary to the project + project["summary"] = summaryText + enrichedProjects = append(enrichedProjects, project) + } + + // Return in format expected by frontend + response.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ + "projects": enrichedProjects, + "total": len(enrichedProjects), + }) +} + +// Enhanced hybrid approach to determine which search type to use +func shouldUseTopicSearch(query string) bool { + // Topic search indicators - phrases and words that suggest semantic search + topicIndicators := []string{ + // Question formats + "about", "related to", "concerning", "regarding", "similar to", + "like", "such as", "involving", "dealing with", "associated with", + + // Command formats + "find", "search", "show", "get", "retrieve", "locate", + "list", "discover", "explore", "suggest", "recommend", + + // Topic phrases + "projects on", "projects about", "projects using", "projects with", + "technology for", "solution for", "approach to", "method for", + "implementation of", "application of", "system for", + + // Domain-specific indicators + "concept", "field", "domain", "area", "topic", "subject", + "category", "theme", "focus", "discipline", "expertise", + + // Question words (if followed by topic) + "what", "which", "where", "who", "how", + } + + // Check for clear indicators first + queryLower := strings.ToLower(query) + for _, indicator := range topicIndicators { + if strings.Contains(queryLower, indicator) { + return true + } + } + + // Direct ID or slug search indicators (likely not topic search) + directSearchIndicators := []string{ + "id:", "project:", "projectid:", + "uuid:", "identifier:", "pid:", + } + + for _, indicator := range directSearchIndicators { + if strings.HasPrefix(queryLower, indicator) { + return false + } + } + + // Short queries with single terms are likely direct searches + if strings.Count(query, " ") <= 1 && len(query) < 20 { + return false + } + + // For queries that are 3+ words, use topic search + words := strings.Fields(query) + if len(words) >= 3 { + return true + } + + // Default to regular search for ambiguous cases + return false +} + +// Format search results to match the expected frontend format +func formatProjectResults(results []map[string]interface{}) []map[string]interface{} { + formattedResults := make([]map[string]interface{}, 0, len(results)) + + for _, result := range results { + fmt.Printf("Processing result: %v\n", result) + + // IMPORTANT CHANGE: Extract project ID correctly + var projectID string + + // Try to get projectId first + if id, ok := result["projectId"].(string); ok && id != "" { + projectID = id + } else { + // If that fails, try p.project_id + if pID, ok := result["p.project_id"].(string); ok && pID != "" { + projectID = pID + } else { + // Try other variations we've seen in the data + for key, value := range result { + if (strings.Contains(strings.ToLower(key), "project_id") || + strings.Contains(strings.ToLower(key), "projectid")) && + value != nil { + if idStr, ok := value.(string); ok && idStr != "" { + projectID = idStr + break + } + } + } + } + } + + // If we still couldn't find a valid ID, log and skip + if projectID == "" { + fmt.Println("Skipping project without ID:", result) + continue + } + + // Get project name - similarly try multiple field names + var projectName string + if name, ok := result["projectName"].(string); ok && name != "" { + projectName = name + } else if name, ok := result["p.name"].(string); ok && name != "" { + projectName = name + } else { + projectName = "Unnamed Project" + } + + // Get description - similarly flexible approach + var description string + if desc, ok := result["description"].(string); ok { + description = desc + } else if desc, ok := result["p.description"].(string); ok { + description = desc + } + + // Initialize tags as an empty SLICE, not nil + tags := make([]string, 0) + + // Process tags if they exist + if tagsInterface, ok := result["tags"]; ok && tagsInterface != nil { + if tagsArray, ok := tagsInterface.([]string); ok { + tags = tagsArray + } else if tagsArray, ok := tagsInterface.([]interface{}); ok { + for _, tag := range tagsArray { + if tagStr, ok := tag.(string); ok { + tags = append(tags, tagStr) + } + } + } + } + + // Fix for the status field + status := "published" // Default value + if statusVal, ok := result["status"].(string); ok { + status = statusVal + } else if statusVal, ok := result["p.status"].(string); ok { + status = statusVal + } + + // Format for frontend + project := map[string]interface{}{ + "id": projectID, + "title": projectName, + "description": description, + "tags": tags, + "status": status, + } + + formattedResults = append(formattedResults, project) + } + + // Debug the final result before returning + fmt.Printf("Formatted %d results\n", len(formattedResults)) + for i, p := range formattedResults { + fmt.Printf("Project %d: ID=%v, Title=%v, Tags=%v\n", i, p["id"], p["title"], p["tags"]) + } + + return formattedResults +} diff --git a/Loop_backend/internal/services/query_service.go b/Loop_backend/internal/services/query_service.go index 63df4e3..88c230f 100644 --- a/Loop_backend/internal/services/query_service.go +++ b/Loop_backend/internal/services/query_service.go @@ -13,7 +13,8 @@ type QueryService interface { TransformQueryToCypher(query string) (string, error) TransformQueryToTopicCypher(query string) (string, error) ExecuteSearchQuery(query string) ([]map[string]interface{}, error) - ExecuteTopicSearchQuery(query string) ([]map[string]interface{}, error) // Add this + ExecuteTopicSearchQuery(query string) ([]map[string]interface{}, error) + GetAllProjects() ([]map[string]interface{}, error) // Add this } // DefaultQueryService implements QueryService @@ -170,3 +171,35 @@ func (s *DefaultQueryService) ExecuteTopicSearchQuery(query string) ([]map[strin return results, nil } + +// GetAllProjects fetches all projects with their basic information +func (s *DefaultQueryService) GetAllProjects() ([]map[string]interface{}, error) { + // Simple Cypher query to fetch all projects with basic information + // cypherQuery := ` + // MATCH (p:Project) + // OPTIONAL MATCH (p)-[:HAS_TAG]->(t:Tag) + // RETURN p.id as projectId, p.name as projectName, + // p.description as description, p.status as status, + // collect(distinct t.name) as tags + // LIMIT 100 + // ` + cypherQuery := `MATCH (p:Project) + OPTIONAL MATCH (p)-[:HAS_TAG]->(t:Tag) + RETURN p.project_id as projectId, p.name as projectName, + p.description as description, + COALESCE(p.status, "published") as status, + collect(distinct t.name) as tags + LIMIT 100 + ` + + fmt.Println("Fetching all projects") + + // Execute the query using the graph repository + results, err := s.graphRepo.ExecuteQuery(cypherQuery, map[string]interface{}{}) + if err != nil { + return nil, fmt.Errorf("failed to fetch all projects: %w", err) + } + fmt.Println("Fetched all projects:", results) + + return results, nil +} diff --git a/Loop_backend/internal/utils/topic_search_prompt.go b/Loop_backend/internal/utils/topic_search_prompt.go index b0c96f1..a014895 100644 --- a/Loop_backend/internal/utils/topic_search_prompt.go +++ b/Loop_backend/internal/utils/topic_search_prompt.go @@ -34,7 +34,81 @@ func GetTopicSearchPrompt(query string, graphRepo repositories.GraphRepository) if relationshipsSection == "" { relationshipsSection = "- HAS_TAG (connects Projects to Tags)\n- USES (connects Projects to Technologies)" } - + /* + return fmt.Sprintf(`---Goal--- + You are a semantic search engine that converts natural language topic queries to Neo4j Cypher queries. + The goal is to find projects semantically related to the specified topic, category, or concept. + + ---Database Schema--- + Node Types: %s + + ---Relationships--- + %s + + ---IMPORTANT: Search Priority--- + Projects are frequently categorized by tags, which is the MOST RELIABLE way to find them by topic. + ALWAYS include tag-based searches as your primary strategy, then fallback to other approaches. + + ---Semantic Search Approach--- + For effective semantic search: + 1. FIRST check tags (this is highest priority) + 2. Check related entities (categories, technologies) + 3. Check project properties (name, description) + 4. Use partial matching with CONTAINS + 5. Consider related concepts and synonyms (agriculture → farming, crops) + 6. For multi-word topics, also search for individual words + + ---Instructions--- + 1. Extract key concepts from the query + 2. Create a search query with TAG MATCHING as the FIRST approach + 3. Use case-insensitive matching with toLower() on both sides + 4. Return project ID as "projectId" and name as "projectName" + 5. Use a UNION approach to combine different search strategies + 6. Return ONLY the Cypher query - no explanations + + ---Examples--- + + Example 1: Finding projects tagged with related concepts + MATCH (p:Project)-[:HAS_TAG]->(t:Tag) + WHERE toLower(t.name) CONTAINS toLower("agriculture") + OR toLower(t.name) CONTAINS toLower("farming") + OR toLower(t.name) CONTAINS toLower("crops") + RETURN DISTINCT p.id as projectId, p.name as projectName + UNION + MATCH (p:Project) + WHERE toLower(p.name) CONTAINS toLower("agriculture") + OR toLower(p.description) CONTAINS toLower("agriculture") + RETURN DISTINCT p.id as projectId, p.name as projectName + + Example 2: For multi-word topics like "machine learning" + MATCH (p:Project)-[:HAS_TAG]->(t:Tag) + WHERE toLower(t.name) CONTAINS toLower("machine learning") + OR (toLower(t.name) CONTAINS toLower("machine") AND toLower(t.name) CONTAINS toLower("learning")) + OR toLower(t.name) CONTAINS toLower("ml") + OR toLower(t.name) CONTAINS toLower("artificial intelligence") + RETURN DISTINCT p.id as projectId, p.name as projectName + UNION + MATCH (p:Project) + WHERE toLower(p.name) CONTAINS toLower("machine learning") + OR toLower(p.description) CONTAINS toLower("machine learning") + RETURN DISTINCT p.id as projectId, p.name as projectName + + Example 3: For technical topics like "predictive analysis" + MATCH (p:Project)-[:HAS_TAG]->(t:Tag) + WHERE toLower(t.name) CONTAINS toLower("predictive") + OR toLower(t.name) CONTAINS toLower("analysis") + OR toLower(t.name) CONTAINS toLower("predictive analysis") + OR toLower(t.name) CONTAINS toLower("data science") + RETURN DISTINCT p.id as projectId, p.name as projectName + UNION + MATCH (p:Project) + WHERE toLower(p.description) CONTAINS toLower("predictive") + OR toLower(p.description) CONTAINS toLower("analysis") + RETURN DISTINCT p.id as projectId, p.name as projectName + + User Query: "%s" + `, strings.Join(entityTypes, ", "), relationshipsSection, query) + }*/ return fmt.Sprintf(`---Goal--- You are a semantic search engine that converts natural language topic queries to Neo4j Cypher queries. The goal is to find projects semantically related to the specified topic, category, or concept. @@ -45,66 +119,62 @@ Node Types: %s ---Relationships--- %s ----IMPORTANT: Search Priority--- -Projects are frequently categorized by tags, which is the MOST RELIABLE way to find them by topic. -ALWAYS include tag-based searches as your primary strategy, then fallback to other approaches. +---IMPORTANT: Node-Agnostic Search Approach--- +To ensure comprehensive results, use NODE-TYPE AGNOSTIC patterns that can match any node type: +1. Use generic patterns like (p:Project)-[]-(n) where n can be ANY node type +2. Check node properties regardless of their type/label +3. Always include project properties (name, description) as a fallback search +4. Return additional fields: description and status ----Semantic Search Approach--- -For effective semantic search: -1. FIRST check tags (this is highest priority) -2. Check related entities (categories, technologies) -3. Check project properties (name, description) -4. Use partial matching with CONTAINS -5. Consider related concepts and synonyms (agriculture → farming, crops) -6. For multi-word topics, also search for individual words +---Search Strategy--- +For effective search: +1. FIRST match through ANY connected nodes with relevant properties +2. THEN check project properties directly +3. Use partial matching with CONTAINS for flexibility +4. Consider related concepts and synonyms ---Instructions--- 1. Extract key concepts from the query -2. Create a search query with TAG MATCHING as the FIRST approach +2. Create a search query using NODE-AGNOSTIC PATTERNS 3. Use case-insensitive matching with toLower() on both sides -4. Return project ID as "projectId" and name as "projectName" +4. Return project ID as "projectId", name as "projectName", plus description and status 5. Use a UNION approach to combine different search strategies -6. Return ONLY the Cypher query - no explanations +6. Include tags or related node names in the results +7. Return ONLY the Cypher query - no explanations ---Examples--- -Example 1: Finding projects tagged with related concepts -MATCH (p:Project)-[:HAS_TAG]->(t:Tag) -WHERE toLower(t.name) CONTAINS toLower("agriculture") - OR toLower(t.name) CONTAINS toLower("farming") - OR toLower(t.name) CONTAINS toLower("crops") -RETURN DISTINCT p.id as projectId, p.name as projectName +Example 1: Finding projects related to agriculture (node-agnostic) +// First search through any connected node +MATCH (p:Project)-[]-(n) +WHERE toLower(n.name) CONTAINS toLower("agriculture") + OR (n.description IS NOT NULL AND toLower(n.description) CONTAINS toLower("agriculture")) +RETURN DISTINCT p.id as projectId, p.name as projectName, + p.description as description, p.status as status, + collect(distinct n.name) as tags UNION +// Then try direct project properties MATCH (p:Project) WHERE toLower(p.name) CONTAINS toLower("agriculture") OR toLower(p.description) CONTAINS toLower("agriculture") -RETURN DISTINCT p.id as projectId, p.name as projectName - -Example 2: For multi-word topics like "machine learning" -MATCH (p:Project)-[:HAS_TAG]->(t:Tag) -WHERE toLower(t.name) CONTAINS toLower("machine learning") - OR (toLower(t.name) CONTAINS toLower("machine") AND toLower(t.name) CONTAINS toLower("learning")) - OR toLower(t.name) CONTAINS toLower("ml") - OR toLower(t.name) CONTAINS toLower("artificial intelligence") -RETURN DISTINCT p.id as projectId, p.name as projectName -UNION -MATCH (p:Project) -WHERE toLower(p.name) CONTAINS toLower("machine learning") - OR toLower(p.description) CONTAINS toLower("machine learning") -RETURN DISTINCT p.id as projectId, p.name as projectName - -Example 3: For technical topics like "predictive analysis" -MATCH (p:Project)-[:HAS_TAG]->(t:Tag) -WHERE toLower(t.name) CONTAINS toLower("predictive") - OR toLower(t.name) CONTAINS toLower("analysis") - OR toLower(t.name) CONTAINS toLower("predictive analysis") - OR toLower(t.name) CONTAINS toLower("data science") -RETURN DISTINCT p.id as projectId, p.name as projectName +RETURN DISTINCT p.id as projectId, p.name as projectName, + p.description as description, p.status as status, + [] as tags + +Example 2: Finding projects that use React (node-agnostic) +MATCH (p:Project)-[]-(n) +WHERE toLower(n.name) = toLower("React") + OR toLower(n.name) CONTAINS toLower("React") +RETURN DISTINCT p.id as projectId, p.name as projectName, + p.description as description, p.status as status, + collect(distinct n.name) as tags UNION MATCH (p:Project) -WHERE toLower(p.description) CONTAINS toLower("predictive") - OR toLower(p.description) CONTAINS toLower("analysis") -RETURN DISTINCT p.id as projectId, p.name as projectName +WHERE toLower(p.name) CONTAINS toLower("React") + OR toLower(p.description) CONTAINS toLower("React") +RETURN DISTINCT p.id as projectId, p.name as projectName, + p.description as description, p.status as status, + [] as tags User Query: "%s" `, strings.Join(entityTypes, ", "), relationshipsSection, query) diff --git a/Loop_backend/main.go b/Loop_backend/main.go index d1e2ef8..4c9c784 100644 --- a/Loop_backend/main.go +++ b/Loop_backend/main.go @@ -31,6 +31,7 @@ type application struct { searchHandler *handlers.SearchHandler summaryService services.SummaryService summaryHandler *handlers.SummaryHandler + chatHandler *handlers.ChatHandler } func main() { @@ -60,6 +61,7 @@ func main() { app.projectHandler.RegisterRoutes(routeRegister) app.searchHandler.RegisterRoutes(routeRegister) app.summaryHandler.RegisterRoutes(routeRegister) + app.chatHandler.RegisterRoutes(routeRegister) // Setup CORS c := cors.New(cors.Options{ @@ -146,6 +148,7 @@ func initializeApp(cfg *config.Config) (*application, error) { projectHandler := handlers.NewProjectHandler(projectService) searchHandler := handlers.NewSearchHandler(queryService, summaryService) summaryHandler := handlers.NewSummaryHandler(summaryService) + chatHandler := handlers.NewChatHandler(queryService, summaryService, provider) return &application{ config: cfg, @@ -159,5 +162,6 @@ func initializeApp(cfg *config.Config) (*application, error) { searchHandler: searchHandler, summaryService: summaryService, summaryHandler: summaryHandler, + chatHandler: chatHandler, }, nil } diff --git a/Loop_frontend/app/chat/actions.ts b/Loop_frontend/app/chat/actions.ts index c92c324..bf172b6 100644 --- a/Loop_frontend/app/chat/actions.ts +++ b/Loop_frontend/app/chat/actions.ts @@ -1,37 +1,12 @@ -export interface ChatMessage { - id: string; - content: string; - type: 'user' | 'llm'; - timestamp: string; -} - -export async function sendMessage(message: string, access_token: string): Promise { - const response = await fetch('/api/chat/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${access_token}` - }, - body: JSON.stringify({ message }) - }); +import { ChatMessage, chatApi } from '../../utils/api'; - if (!response.ok) { - throw new Error('Failed to send message'); - } +// Re-export the ChatMessage type for components that import from here +export type { ChatMessage }; - return response.json(); +export async function sendMessage(message: string, access_token: string): Promise { + return chatApi.sendMessage(message, access_token); } export async function fetchChatHistory(access_token: string): Promise { - const response = await fetch('/api/chat/history', { - headers: { - 'Authorization': `Bearer ${access_token}` - } - }); - - if (!response.ok) { - throw new Error('Failed to fetch chat history'); - } - - return response.json(); + return chatApi.fetchChatHistory(access_token); } diff --git a/Loop_frontend/app/chat/page.tsx b/Loop_frontend/app/chat/page.tsx index 95badac..58f0d61 100644 --- a/Loop_frontend/app/chat/page.tsx +++ b/Loop_frontend/app/chat/page.tsx @@ -41,9 +41,15 @@ export default function ChatPage() { const loadChatHistory = async () => { try { const history = await fetchChatHistory(access_token!); - setMessages(history); + + // Only replace messages if there's actual history + if (history && history.length > 0) { + setMessages(history); + } + // Otherwise, keep the initial welcome messages } catch (error) { console.error("Failed to load chat history:", error); + // On error, we keep the initial messages too } }; diff --git a/Loop_frontend/package.json b/Loop_frontend/package.json index 27c1369..4fa48e7 100644 --- a/Loop_frontend/package.json +++ b/Loop_frontend/package.json @@ -58,6 +58,7 @@ "postcss": "8.4.38", "react": "18.3.1", "react-dom": "18.3.1", + "react-icons": "^5.5.0", "sonner": "^2.0.3", "tailwind-variants": "0.1.20", "tailwindcss": "3.4.3", diff --git a/Loop_frontend/utils/api.ts b/Loop_frontend/utils/api.ts index 606bf52..2095b8f 100644 --- a/Loop_frontend/utils/api.ts +++ b/Loop_frontend/utils/api.ts @@ -154,6 +154,14 @@ export async function apiRequest( } } +// Add ChatMessage interface +export interface ChatMessage { + id: string; + content: string; + type: 'user' | 'llm'; + timestamp: string; +} + // Type-safe API functions export const api = { auth: { @@ -223,3 +231,37 @@ export const api = { }, }, }; + +// Chat API Functions +export const chatApi = { + sendMessage: async (message: string, token: string): Promise => { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/chat/conversation`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ message }) + }); + + if (!response.ok) { + throw new Error('Failed to send message'); + } + + return response.json(); + }, + + fetchChatHistory: async (token: string): Promise => { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/chat/history`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error('Failed to fetch chat history'); + } + + return response.json(); + } +}; diff --git a/scripts/convert_project_content.py b/scripts/convert_project_content.py index f70d92f..6a1cac5 100644 --- a/scripts/convert_project_content.py +++ b/scripts/convert_project_content.py @@ -21,8 +21,9 @@ def convert_project_content(input_file, output_file): with open(output_file, 'w') as outfile: json.dump(converted_data, outfile, indent=2) -input_dir = "dataset" -output_dir = "converted_dataset" +input_dir = "d:/sem_6/code/genai-main-1/Loop/converted_dataset_temp" +output_dir = "d:/sem_6/code/genai-main-1/Loop/converted_dataset_temp_v2" +os.makedirs(output_dir, exist_ok=True) for filename in os.listdir(input_dir): if filename.endswith(".json"): input_filepath = os.path.join(input_dir, filename) diff --git a/scripts/create_projects_script.py b/scripts/create_projects_script.py index dd2e66f..896ca1d 100644 --- a/scripts/create_projects_script.py +++ b/scripts/create_projects_script.py @@ -2,9 +2,11 @@ import json import requests import random +import time # Define input directory -input_dir = "dataset" +#input_dir = "dataset" +input_dir = "D:/sem_6/code/genai-main-1/Loop/converted_dataset_temp" # API Endpoint api_url = "http://localhost:8080/api/project/create" @@ -20,8 +22,8 @@ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYTMxYjY5NDItNDUyOS00MWUxLWFiYTUtODUxNDdmYTFjNjdhIiwiZXhwIjoxNzUwOTk2MDE0LCJpYXQiOjE3NDIzNTYwMTR9.WsKpJpXq1k-3qY2atgnIpaehAVMMp4brq9Sild69cKs", ] -auth_token = random.choice(auth_tokens_list) - +#auth_token = random.choice(auth_tokens_list) +auth_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMjQzNDM1NjgtZmY3OS00MTljLTgwMDUtN2Y2MGQwOTFkZDk1IiwiZXhwIjoxNzUzNzgzNjI2LCJpYXQiOjE3NDUxNDM2MjZ9.KA5jraKJjJSJixcOg9f45lpD8d2Ch9wQZhbt-n0dy8U" # Headers headers = { "Content-Type": "application/json", @@ -40,6 +42,9 @@ # Send POST request response = requests.post(api_url, json=json_data, headers=headers) + wait_time = 10 + random.randint(0, 5) + print(f"Waiting {wait_time} seconds before next request...") + time.sleep(wait_time) # Print response print(f"Response for {filename}: {response.status_code} - {response.text}")