All checks were successful
Deploy Smart Search Backend Test / deploy (push) Successful in 1m34s
322 lines
9.8 KiB
Go
322 lines
9.8 KiB
Go
package ai
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"regexp"
|
||
"strings"
|
||
|
||
"git.techease.ru/Smart-search/smart-search-back/internal/model"
|
||
"git.techease.ru/Smart-search/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:"address"`
|
||
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: "gpt-4.1-mini",
|
||
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 func() { _ = 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",
|
||
},
|
||
}
|
||
}
|