Basdat/internal/services/ai_service.go
2025-12-20 00:01:08 +07:00

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)
}