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:"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 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", }, } }