add service

This commit is contained in:
vallyenfail
2026-01-17 17:39:33 +03:00
parent 1376ff9188
commit d959dcca96
82 changed files with 25041 additions and 1 deletions

321
internal/ai/perplexity.go Normal file
View 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",
},
}
}