300 lines
8.2 KiB
Go
300 lines
8.2 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"lost-and-found/internal/models"
|
|
"lost-and-found/internal/repositories"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type AIService struct {
|
|
db *gorm.DB
|
|
chatRepo *repositories.ChatRepository
|
|
itemRepo *repositories.ItemRepository
|
|
lostItemRepo *repositories.LostItemRepository
|
|
groqAPIKey string
|
|
groqModel string
|
|
}
|
|
|
|
func NewAIService(db *gorm.DB) *AIService {
|
|
model := os.Getenv("GROQ_MODEL")
|
|
if model == "" {
|
|
model = "llama-3.3-70b-versatile" // Default model
|
|
}
|
|
|
|
return &AIService{
|
|
db: db,
|
|
chatRepo: repositories.NewChatRepository(db),
|
|
itemRepo: repositories.NewItemRepository(db),
|
|
lostItemRepo: repositories.NewLostItemRepository(db),
|
|
groqAPIKey: os.Getenv("GROQ_API_KEY"),
|
|
groqModel: model,
|
|
}
|
|
}
|
|
|
|
type ChatRequest struct {
|
|
Message string `json:"message" binding:"required"`
|
|
}
|
|
|
|
// Groq API Request Structure
|
|
type GroqRequest struct {
|
|
Model string `json:"model"`
|
|
Messages []GroqMessage `json:"messages"`
|
|
Temperature float64 `json:"temperature,omitempty"`
|
|
MaxTokens int `json:"max_tokens,omitempty"`
|
|
TopP float64 `json:"top_p,omitempty"`
|
|
Stream bool `json:"stream"`
|
|
}
|
|
|
|
type GroqMessage struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
// Groq API Response Structure
|
|
type GroqResponse struct {
|
|
ID string `json:"id"`
|
|
Object string `json:"object"`
|
|
Created int64 `json:"created"`
|
|
Model string `json:"model"`
|
|
Choices []struct {
|
|
Index int `json:"index"`
|
|
Message struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
} `json:"message"`
|
|
FinishReason string `json:"finish_reason"`
|
|
} `json:"choices"`
|
|
Usage struct {
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
CompletionTokens int `json:"completion_tokens"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
} `json:"usage"`
|
|
}
|
|
|
|
func (s *AIService) ProcessChat(userID uint, message string) (*models.ChatMessage, error) {
|
|
// Build context from user data
|
|
context, err := s.buildUserContext(userID, message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Detect intent
|
|
intent := s.detectIntent(message)
|
|
|
|
// Build prompt with context
|
|
systemPrompt := s.buildSystemPrompt()
|
|
userPrompt := s.buildUserPrompt(message, context, intent)
|
|
|
|
// Call Groq API
|
|
response, err := s.callGroqAPI(systemPrompt, userPrompt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Save to database
|
|
chat := &models.ChatMessage{
|
|
UserID: userID,
|
|
Message: message,
|
|
Response: response,
|
|
Intent: intent,
|
|
ConfidenceScore: 85.0,
|
|
}
|
|
|
|
if err := s.chatRepo.Create(chat); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return chat, nil
|
|
}
|
|
|
|
func (s *AIService) buildUserContext(userID uint, message string) (string, error) {
|
|
var context strings.Builder
|
|
|
|
// Get user's lost items
|
|
lostItems, _, _ := s.lostItemRepo.FindByUser(userID, 1, 5)
|
|
if len(lostItems) > 0 {
|
|
context.WriteString("\n📋 Barang yang dilaporkan hilang:\n")
|
|
for _, item := range lostItems {
|
|
context.WriteString(fmt.Sprintf("- %s (%s) - Status: %s\n",
|
|
item.Name, item.Category.Name, item.Status))
|
|
}
|
|
}
|
|
|
|
// Search for relevant found items if user is looking for something
|
|
if strings.Contains(strings.ToLower(message), "cari") ||
|
|
strings.Contains(strings.ToLower(message), "temukan") {
|
|
items, _, _ := s.itemRepo.FindAll(1, 5, "unclaimed", "", message)
|
|
if len(items) > 0 {
|
|
context.WriteString("\n🔍 Barang ditemukan yang relevan:\n")
|
|
for _, item := range items {
|
|
context.WriteString(fmt.Sprintf("- ID: %d, %s (%s) - Lokasi: %s\n",
|
|
item.ID, item.Name, item.Category.Name, item.Location))
|
|
}
|
|
}
|
|
}
|
|
|
|
return context.String(), nil
|
|
}
|
|
|
|
func (s *AIService) detectIntent(message string) string {
|
|
msgLower := strings.ToLower(message)
|
|
|
|
searchKeywords := []string{"cari", "temukan", "ada", "lihat", "ditemukan"}
|
|
reportKeywords := []string{"hilang", "kehilangan", "lapor", "laporkan"}
|
|
claimKeywords := []string{"klaim", "ambil", "punya saya", "milik saya"}
|
|
|
|
for _, kw := range searchKeywords {
|
|
if strings.Contains(msgLower, kw) {
|
|
return models.IntentSearchItem
|
|
}
|
|
}
|
|
|
|
for _, kw := range reportKeywords {
|
|
if strings.Contains(msgLower, kw) {
|
|
return models.IntentReportLost
|
|
}
|
|
}
|
|
|
|
for _, kw := range claimKeywords {
|
|
if strings.Contains(msgLower, kw) {
|
|
return models.IntentClaimHelp
|
|
}
|
|
}
|
|
|
|
return models.IntentGeneral
|
|
}
|
|
|
|
func (s *AIService) buildSystemPrompt() string {
|
|
return `Kamu adalah asisten AI untuk sistem Lost & Found kampus bernama "FindItBot".
|
|
|
|
Tugasmu adalah membantu mahasiswa dan staff dengan:
|
|
1. 🔍 Mencari barang yang hilang/ditemukan
|
|
2. 📝 Memandu proses pelaporan barang hilang
|
|
3. ✅ Menjelaskan proses klaim barang
|
|
4. ❓ Menjawab pertanyaan umum tentang sistem
|
|
|
|
Aturan penting:
|
|
- Jawab dengan ramah, profesional, dan membantu
|
|
- Gunakan Bahasa Indonesia yang jelas
|
|
- Jika ada data barang yang relevan, sebutkan ID dan detailnya
|
|
- Untuk pelaporan, tanyakan: nama barang, kategori, lokasi, tanggal hilang, deskripsi
|
|
- Untuk klaim, jelaskan proses verifikasi yang diperlukan
|
|
- Gunakan emoji yang sesuai untuk memperjelas informasi
|
|
- Prioritaskan informasi dari konteks yang diberikan
|
|
|
|
Contoh respons yang baik:
|
|
"🔍 Saya menemukan 2 barang yang mungkin cocok:
|
|
1. ID: 123 - Dompet Kulit (Kategori: Wallet) - Ditemukan di Perpustakaan
|
|
2. ID: 124 - Dompet Hitam (Kategori: Wallet) - Ditemukan di Kantin
|
|
|
|
Apakah salah satu dari ini milik Anda? Anda bisa klaim dengan menyebutkan ID barangnya."`
|
|
}
|
|
|
|
func (s *AIService) buildUserPrompt(message, context, intent string) string {
|
|
var prompt strings.Builder
|
|
|
|
if context != "" {
|
|
prompt.WriteString("KONTEKS PENGGUNA:\n")
|
|
prompt.WriteString(context)
|
|
prompt.WriteString("\n\n")
|
|
}
|
|
|
|
prompt.WriteString(fmt.Sprintf("INTENT TERDETEKSI: %s\n\n", intent))
|
|
prompt.WriteString(fmt.Sprintf("PERTANYAAN: %s\n\n", message))
|
|
prompt.WriteString("Berikan respons yang membantu berdasarkan konteks di atas.")
|
|
|
|
return prompt.String()
|
|
}
|
|
|
|
func (s *AIService) callGroqAPI(systemPrompt, userPrompt string) (string, error) {
|
|
if s.groqAPIKey == "" {
|
|
return "", errors.New("GROQ_API_KEY not configured")
|
|
}
|
|
|
|
url := "https://api.groq.com/openai/v1/chat/completions"
|
|
|
|
reqBody := GroqRequest{
|
|
Model: s.groqModel,
|
|
Messages: []GroqMessage{
|
|
{
|
|
Role: "system",
|
|
Content: systemPrompt,
|
|
},
|
|
{
|
|
Role: "user",
|
|
Content: userPrompt,
|
|
},
|
|
},
|
|
Temperature: 0.7,
|
|
MaxTokens: 1024,
|
|
TopP: 0.95,
|
|
Stream: false,
|
|
}
|
|
|
|
jsonData, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal request: %v", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create request: %v", err)
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+s.groqAPIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to call Groq API: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read response: %v", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("Groq API error (status %d): %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var groqResp GroqResponse
|
|
if err := json.Unmarshal(body, &groqResp); err != nil {
|
|
return "", fmt.Errorf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if len(groqResp.Choices) == 0 {
|
|
return "", errors.New("no response from Groq API")
|
|
}
|
|
|
|
return groqResp.Choices[0].Message.Content, nil
|
|
}
|
|
|
|
func (s *AIService) GetChatHistory(userID uint, limit int) ([]models.ChatMessageResponse, error) {
|
|
chats, err := s.chatRepo.GetUserChatHistory(userID, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var responses []models.ChatMessageResponse
|
|
for _, chat := range chats {
|
|
responses = append(responses, chat.ToResponse())
|
|
}
|
|
|
|
return responses, nil
|
|
}
|
|
|
|
func (s *AIService) ClearChatHistory(userID uint) error {
|
|
return s.chatRepo.DeleteUserHistory(userID)
|
|
} |