add service
This commit is contained in:
321
internal/ai/perplexity.go
Normal file
321
internal/ai/perplexity.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"smart-search-back/internal/model"
|
||||
"smart-search-back/pkg/errors"
|
||||
)
|
||||
|
||||
type PerplexityClient struct {
|
||||
apiKey string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type perplexityRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []perplexityMessage `json:"messages"`
|
||||
}
|
||||
|
||||
type perplexityMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type perplexityResponse struct {
|
||||
Choices []struct {
|
||||
Message perplexityMessage `json:"message"`
|
||||
} `json:"choices"`
|
||||
Usage *struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
} `json:"usage,omitempty"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type supplierData struct {
|
||||
CompanyName string `json:"company_name"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"adress"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func NewPerplexityClient(apiKey string) *PerplexityClient {
|
||||
return &PerplexityClient{
|
||||
apiKey: apiKey,
|
||||
client: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PerplexityClient) isMockMode() bool {
|
||||
return c.apiKey == ""
|
||||
}
|
||||
|
||||
func (c *PerplexityClient) FindSuppliers(tzText string) ([]*model.Supplier, int, int, error) {
|
||||
if c.isMockMode() {
|
||||
return c.generateMockSuppliers(), 1000, 500, nil
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`Ты — эксперт по поиску поставщиков на российском рынке.
|
||||
|
||||
ЗАДАЧА:
|
||||
Найти компании-поставщики/производители для технического задания:
|
||||
%s
|
||||
|
||||
ОПРЕДЕЛЕНИЕ ТИПА ПОИСКА:
|
||||
- Если ТЗ требует производства/изготовления → ищи производителей
|
||||
- Если ТЗ требует готовый товар/услугу → ищи компании, которые это продают
|
||||
|
||||
КРИТЕРИИ:
|
||||
1. Максимальная релевантность
|
||||
2. Действующие компании с полными контактами
|
||||
3. Приоритет: производители > дистрибьюторы
|
||||
|
||||
ПРИМЕРЫ ОТВЕТОВ:
|
||||
|
||||
Пример 1 - ТЗ: "Поставка офисной мебели: столы 20 шт, стулья 50 шт"
|
||||
[
|
||||
{
|
||||
"company_name": "ООО Мебельная фабрика Союз",
|
||||
"email": "zakaz@mebelsoyuz.ru",
|
||||
"phone": "+7 495 123-45-67",
|
||||
"adress": "г. Москва, ул. Промышленная, д. 15",
|
||||
"url": ""
|
||||
}
|
||||
]
|
||||
|
||||
Пример 2 - ТЗ: "Производство кованых изделий: ворота, решётки, перила"
|
||||
[
|
||||
{
|
||||
"company_name": "ООО Кузня Премиум",
|
||||
"email": "info@kuzniya.ru",
|
||||
"phone": "",
|
||||
"adress": "г. Москва, ул. Заводская, д. 42",
|
||||
"url": "www.mebelsoyuz.ru"
|
||||
}
|
||||
]
|
||||
|
||||
ФОРМАТ ОТВЕТА - ТОЛЬКО JSON:
|
||||
[
|
||||
{
|
||||
"company_name": "...",
|
||||
"email": "...",
|
||||
"phone": "...",
|
||||
"adress": "...",
|
||||
"url": "..."
|
||||
}
|
||||
]
|
||||
|
||||
ПРАВИЛА:
|
||||
- Минимум 15 компаний, максимум 100
|
||||
- Только валидный JSON массив, без текста вокруг
|
||||
- email и url всегда заполнены (не null)
|
||||
- Если другие поля пустые, то можно оставить пустую строку -> ""
|
||||
- Сортируй по релевантности`, tzText)
|
||||
|
||||
reqBody := perplexityRequest{
|
||||
Model: "llama-3.1-sonar-large-128k-online",
|
||||
Messages: []perplexityMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to marshal request", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://api.perplexity.ai/chat/completions", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to create request", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to send request", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to read response", err)
|
||||
}
|
||||
|
||||
var aiResp perplexityResponse
|
||||
if err := json.Unmarshal(body, &aiResp); err != nil {
|
||||
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to unmarshal response", err)
|
||||
}
|
||||
|
||||
if aiResp.Error != nil {
|
||||
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, aiResp.Error.Message, nil)
|
||||
}
|
||||
|
||||
if len(aiResp.Choices) == 0 {
|
||||
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "no choices in response", nil)
|
||||
}
|
||||
|
||||
content := aiResp.Choices[0].Message.Content
|
||||
|
||||
content = strings.TrimPrefix(content, "```json")
|
||||
content = strings.TrimPrefix(content, "```")
|
||||
content = strings.TrimSuffix(content, "```")
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
var supplierDataList []supplierData
|
||||
if err := json.Unmarshal([]byte(content), &supplierDataList); err != nil {
|
||||
re := regexp.MustCompile(`\[[\s\S]*\]`)
|
||||
match := re.FindString(content)
|
||||
if match != "" {
|
||||
if err := json.Unmarshal([]byte(match), &supplierDataList); err != nil {
|
||||
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to parse suppliers response", err)
|
||||
}
|
||||
} else {
|
||||
return nil, 0, 0, errors.NewInternalError(errors.AIAPIError, "failed to parse suppliers response", err)
|
||||
}
|
||||
}
|
||||
|
||||
suppliers := make([]*model.Supplier, 0, len(supplierDataList))
|
||||
for _, sd := range supplierDataList {
|
||||
suppliers = append(suppliers, &model.Supplier{
|
||||
Name: sd.CompanyName,
|
||||
Email: sd.Email,
|
||||
Phone: sd.Phone,
|
||||
Address: sd.Address,
|
||||
URL: sd.URL,
|
||||
})
|
||||
}
|
||||
|
||||
promptTokens := 0
|
||||
responseTokens := 0
|
||||
if aiResp.Usage != nil {
|
||||
promptTokens = aiResp.Usage.PromptTokens
|
||||
responseTokens = aiResp.Usage.CompletionTokens
|
||||
}
|
||||
|
||||
return suppliers, promptTokens, responseTokens, nil
|
||||
}
|
||||
|
||||
func (c *PerplexityClient) generateMockSuppliers() []*model.Supplier {
|
||||
return []*model.Supplier{
|
||||
{
|
||||
Name: "ООО Поставщик-1 (Mock)",
|
||||
Email: "supplier1@example.com",
|
||||
Phone: "+7 (495) 123-45-67",
|
||||
Address: "г. Москва, ул. Примерная, д. 1",
|
||||
URL: "https://supplier1.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Поставщик-2 (Mock)",
|
||||
Email: "supplier2@example.com",
|
||||
Phone: "+7 (495) 234-56-78",
|
||||
Address: "г. Москва, ул. Примерная, д. 2",
|
||||
URL: "https://supplier2.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Поставщик-3 (Mock)",
|
||||
Email: "supplier3@example.com",
|
||||
Phone: "+7 (495) 345-67-89",
|
||||
Address: "г. Москва, ул. Примерная, д. 3",
|
||||
URL: "https://supplier3.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Производитель-1 (Mock)",
|
||||
Email: "producer1@example.com",
|
||||
Phone: "+7 (495) 456-78-90",
|
||||
Address: "г. Санкт-Петербург, ул. Тестовая, д. 10",
|
||||
URL: "https://producer1.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Производитель-2 (Mock)",
|
||||
Email: "producer2@example.com",
|
||||
Phone: "+7 (495) 567-89-01",
|
||||
Address: "г. Санкт-Петербург, ул. Тестовая, д. 20",
|
||||
URL: "https://producer2.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Дистрибьютор-1 (Mock)",
|
||||
Email: "distributor1@example.com",
|
||||
Phone: "+7 (495) 678-90-12",
|
||||
Address: "г. Казань, ул. Демо, д. 5",
|
||||
URL: "https://distributor1.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Дистрибьютор-2 (Mock)",
|
||||
Email: "distributor2@example.com",
|
||||
Phone: "+7 (495) 789-01-23",
|
||||
Address: "г. Казань, ул. Демо, д. 15",
|
||||
URL: "https://distributor2.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Импортер-1 (Mock)",
|
||||
Email: "importer1@example.com",
|
||||
Phone: "+7 (495) 890-12-34",
|
||||
Address: "г. Новосибирск, ул. Примера, д. 100",
|
||||
URL: "https://importer1.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Импортер-2 (Mock)",
|
||||
Email: "importer2@example.com",
|
||||
Phone: "+7 (495) 901-23-45",
|
||||
Address: "г. Новосибирск, ул. Примера, д. 200",
|
||||
URL: "https://importer2.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Оптовик-1 (Mock)",
|
||||
Email: "wholesale1@example.com",
|
||||
Phone: "+7 (495) 012-34-56",
|
||||
Address: "г. Екатеринбург, ул. Тестовая, д. 50",
|
||||
URL: "https://wholesale1.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Оптовик-2 (Mock)",
|
||||
Email: "wholesale2@example.com",
|
||||
Phone: "+7 (495) 123-45-67",
|
||||
Address: "г. Екатеринбург, ул. Тестовая, д. 60",
|
||||
URL: "https://wholesale2.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Фабрика-1 (Mock)",
|
||||
Email: "factory1@example.com",
|
||||
Phone: "+7 (495) 234-56-78",
|
||||
Address: "г. Нижний Новгород, Промзона, д. 1",
|
||||
URL: "https://factory1.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Фабрика-2 (Mock)",
|
||||
Email: "factory2@example.com",
|
||||
Phone: "+7 (495) 345-67-89",
|
||||
Address: "г. Нижний Новгород, Промзона, д. 2",
|
||||
URL: "https://factory2.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Завод-1 (Mock)",
|
||||
Email: "plant1@example.com",
|
||||
Phone: "+7 (495) 456-78-90",
|
||||
Address: "г. Челябинск, Индустриальная, д. 10",
|
||||
URL: "https://plant1.example.com",
|
||||
},
|
||||
{
|
||||
Name: "ООО Завод-2 (Mock)",
|
||||
Email: "plant2@example.com",
|
||||
Phone: "+7 (495) 567-89-01",
|
||||
Address: "г. Челябинск, Индустриальная, д. 20",
|
||||
URL: "https://plant2.example.com",
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user