Обход ограничений на доступ к OpenAI API

Для работы нужен доступ к OpenAI API. Мне не хотелось искать способы временной регистрации зарубежных телефонов для получения SMS от OpenAI и оплаты сервиса по карте российских банков.

Я довольно давно пользовался сервисом https://chatgpt-accounts.ru/ для получения аккаунтов для доступа к API. Сервис проверенный, работает нормально, но только как-то зашел на него, а там только доступ к web интерфейсу OpenAI, а вот API нет.

Нашел сервис проксирования запросов к OpenAI API: https://proxyapi.ru/ Но в документации к сервису описан вызов через указание ключа и url прокси в аргументах конструктора.

from openai import OpenAI

client = OpenAI(
    api_key="{PROXY_API_KEY}",
    base_url="https://api.proxyapi.ru/openai/v1",
)

chat_completion = client.chat.completions.create(
    model="gpt-3.5-turbo", messages=[{"role": "user", "content": "Hello world"}]
)

Мне нужен был способ передачи параметров через переменную окружения, поскольку нужна была векторная база FAISS и запускал я код в Colab. Нашел название переменной окружения:

from google.colab import userdata
os.environ["OPENAI_API_KEY"] = userdata.get('OPENAI_API_KEY_PROXY')
os.environ["OPENAI_API_BASE"] = "https://api.proxyapi.ru/openai/v1" 

При использовании прокси сервера для доступа к API Proxy используется переменная окружения «OPENAI_PROXY»:

os.environ["OPENAI_PROXY"] = "http://your-corporate-proxy:8080"
Рубрика: IT рецепты | Метки: | Оставить комментарий

Распознавание голоса Tinkoff Voicekit

Для обработки сохраненных аудифовайлов можно использовать сервис для распознавания аудио Tinkoff.

В Google Colab подгрузим нужные бибиотеки:

!pip install tinkoff-voicekit-client protobuf==3.20.3
!pip install pydub # установка библиотеки pydub

from pydub import AudioSegment

Посмотрим, что за параметры у загруженного аудиофайла:

# чтение из файла любого формата
music = AudioSegment.from_file(file='call.mp3', format='mp3')

print("Продолжительность аудио, сек:", music.duration_seconds)
print("Частота дискретизации:", music.frame_rate)
print("Количество каналов:", music.channels)

Результат:

Продолжительность аудио, сек: 90.0
Частота дискретизации: 16000
Количество каналов: 2

Обрежем первые 15 секунд аудио. На моей записи там гудки. Не влияет на распознавание. Можно и не делать.

# представление 15 секунд в миллисекундах
time_to_cut = 15 * 1000

# обрезка файла при помощи индексации
#music[time_to_cut:]
# сохраним фрагмент в файл с заданной миллисекунды
music[time_to_cut:].export('cutted.mp3', format='mp3')

Для использования сервиса Tinkoff VoiceKit необходима регистрация на платформе https://software.tinkoff.ru/auth/login/

После регистрации на балансе будет 1000 рублей, которые можно использовать для тестирования сервиса.

После регистрации необходимо создать и сохранить 2 ключа: API-key и SECRET-key.

  • API_KEY можно сгенерировать в личном кабинете в разделе VoiceKit в любой момент времени.
  • SECRET_KEY генерируется автоматически только при получении первого API_KEY, потом SECRET_KEY будет недоступен, поэтому крайне ВАЖНО сразу его сохранить.

Передаем параметры для авторизации:

import getpass

# передаем API_KEY
API_KEY = getpass.getpass("Tinkoff API Key:")
# передаем SECRET_KEY
SECRET_KEY = getpass.getpass("Tinkoff SECRET Key:")

Метод Recognize

Используется для распознавания аудиофайлов разных форматов (mp3, wav, s16). Для транскрибации текста метод принимает словарь параметров:

audio_config = {"encoding": "MPEG_AUDIO",  
                "sample_rate_hertz": 16000,
                "num_channels": 2}

"encoding" - кодировка, может быть: 'LINEAR16', 'ALAW', 'MULAW', 'LINEAR32F', 'RAW_OPUS', 'MPEG_AUDIO';
"sample_rate_hertz" - частота дискретизации записи;
"num_channels" - количество каналов записи (1 или 2).

Для распознавания речи используем следующий код:

from tinkoff_voicekit_client import ClientSTT
from pprint import pprint

# создаем клиент, передаем ключи
client = ClientSTT(API_KEY, SECRET_KEY)

# указываем параметры аудио
audio_config = {
    "encoding": "MPEG_AUDIO",
    "sample_rate_hertz": music.frame_rate,
    "num_channels": music.channels,
    "enable_automatic_punctuation": True,
    }

# вызываем метод recognize
response = client.recognize("cutted.mp3", audio_config)
pprint(response)

При распечатке данных возвращаемого объекта:

{'results': [{'alternatives': [{'confidence': -3.803578,
                                'transcript': 'Алло.',
                                'words': [{'confidence': 0.0,
                                           'end_time': '3.270s',
                                           'start_time': '3.060s',
                                           'word': 'алло'}]}],
              'channel': 1,
              'end_time': '3.270s',
              'start_time': '3.060s'},
             {'alternatives': [{'confidence': -4.384712,
                                'transcript': 'Здравствуйте, Алина.',
                                'words': [{'confidence': 0.0,
                                           'end_time': '4.320s',
                                           'start_time': '3.870s',
                                           'word': 'здравствуйте'},
                                          {'confidence': 0.0,
                                           'end_time': '4.710s',
                                           'start_time': '4.410s',
                                           'word': 'алина'}]}],
              'channel': 0,
              'end_time': '4.710s',
              'start_time': '3.870s'},
...
]}

В исходной записи звонок записан на два канала:

  • 0-й канал — то, что говорит менеджер.
  • 1-й канал — речь клиента.
for fragment in response['results']:
  alternatives = fragment['alternatives']
  for alternative in alternatives:
    str = ""
    if fragment['channel'] == 0:
      str += "Менеджер:"
    else:
      str += "Клиент:"
    str += " " + alternative["transcript"] + "\t[Start: " + fragment["start_time"] + ", End: " + fragment["end_time"] + "]"
    print(str)

Результат распознавания:

Клиент: Алло.	[Start: 3.060s, End: 3.270s]
Менеджер: Здравствуйте, Алина.	[Start: 3.870s, End: 4.710s]

Для удобства можно перевести время в секундах в миллисекунды, чтобы можно было оперативно прослушать фразу, которая плохо распозналась:

def strTimeToInt(strtime):
  strtime = strtime.replace("s","")
  return int(float(strtime)*1000)

strTimeToInt("3.060s")

for fragment in response['results']:
  alternatives = fragment['alternatives']
  for alternative in alternatives:
    text = ""
    if fragment['channel'] == 0:
      text += "Менеджер:"
    else:
      text += "Клиент:"
    start_time = str(strTimeToInt(fragment["start_time"]))
    end_time = str(strTimeToInt(fragment["end_time"]))
    text += " " + alternative["transcript"] + "\tmusic[" +  start_time + ":" + end_time + "]"
    print(text)

Тогда получаем:

Клиент: Алло.	music[3060:3270]
Менеджер: Здравствуйте, Алина.	music[3870:4710]

Вставляя код

music[3060:3270]

в cell Colab получаем возможность прослушать выбранный фрагмент.

Рубрика: IT рецепты | Оставить комментарий

FastAPI на Colab через ngrok и uvicorn на Python

Столкнулся тут с проблемкой. Нужно было запустить FastAPI через uvicorn. При этом обращения с сервера должны были идти на API OpenAI. Код писал на Python в VSCode. Запустить надо было по-быстрому. Нормальных VPN не нашел. «Спасибо» добрякам из Роскомнадзора…

В общем, как самый быстрый способ проверки работоспособностии работы OpenAI API с FastAPI получился такой:

  • На ПК установлен Yandex Disk. На нем создана папка проекта.
  • В VSCode проект на Python сохраняется на Yandex Disk.
  • В Colab во первых строках кода исходники проекта скачиваются в локальную папку и добавляются в путь для поиска модулей.
  • Как скачать и развернуть файлы проекта в Google Colab с Yandex Disk описано в другой статье.
  • Подготавливается FastAPI.
  • Поднимается ngrok.
  • Запускается uvicorn.

Для подготовки FastAPI к запуску пример кода:

!pip install fastapi nest-asyncio pyngrok uvicorn

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
    allow_methods=['*'],
    allow_headers=['*'],
)

# класс с типами данных параметров 
class Item(BaseModel): 
    text: str

# функция обработки get запроса + декоратор 
@app.get("/")
async def read_root():
    return {"message": "answer"}

@app.get("/api/access_counter")
async def get_accesscouner():
  counter = chunk.get_accesscouner()
  return {"access_counter": counter}

# функция обработки post запроса + декоратор 
@app.post("/api/get_answer")
async def get_answer(question: Item):
    answer = chunk.get_answer(query=question.text)
    return {"message": answer}

Далее поднимаем ngrok. Передаем токен полученный при регистрации на Ngrok и предварительно сохраненый в .env. Подгружаем его через load_dotenv из папки загруженными с Yandex Disk исходниками:

  dotenv_path = "/content/" + dirs[0] + "/.env"
  load_dotenv(dotenv_path)

и запускаем веб сервер uvicorn:

import nest_asyncio
from pyngrok import ngrok
import uvicorn

ngrok.set_auth_token(os.environ.get("NGROK_AUTHTOKEN"))

ngrok_tunnel = ngrok.connect(8000)
print('Public URL:', ngrok_tunnel.public_url)
nest_asyncio.apply()
uvicorn.run(app, port=8000)

Сервер uvicorn повисает на временно сгенерированном ngrok внешнем домене.

Для бесплатных пользователей при создании аккаунта на Ngrok предоставляется один домен, который нужно создать в консоли ngrok (CloudEdge -> Domains). Выглядит он как-то так: «xxx.ngrok-free.app». Несколько доменов можно создать, купив платную подписку, но с проплатой из России проблемки. 🙂

Веб сервер Uvicorn туннелируется (пробрасывается) из Colab наружу через Ngrok и становится доступным для обращений по сгенерированному на Ngrok доменному имени. При обращениях по доменному имени запросы перенаправляются через Ngrok в Colab на Uvicorn и ответы возвращаются обратно.

ngrok_tunnel = ngrok.connect(8000, domain="your_domain.ngrok-free.app")

Возможно, есть достойные альтернативы ngrok, но времени на эксперименты не было.

Public URL: https://bb2f-34-16-145-243.ngrok-free.app
INFO:     Started server process [23974]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     81.222.179.30:0 - "GET /api/access_counter HTTP/1.1" 200 OK
INFO:     81.222.179.30:0 - "GET /api/access_counter HTTP/1.1" 200 OK
INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [23974]

К сожалению, ячейка Colab запускается не асинхронно, т.е. управление с неё не передается на следующую ячейку. Гипотетически можно это обойти, запустив сервер через командную строку.

Для простоты я для тестирования использовал Postman. Вот так выглядит отправка запроса к серверу поднятому через ngrok на Colab.

Нюансы отправки REST API с помощью JavaScript через Ngrok

При отправке GET запросов на сервер через ngrok наверняка столкнетесь с тем, что стандартный JavaScript запрос выдает ошибку вида:

"Unexpected token '<', \"<!DOCTYPE \"... is not valid JSON"

или

Unexpected token '<', \"<!DOCTYPE ";... is not valid JSON"

При этом POST запрос у меня почему-то проходил без вопросов. Я сломал всю голову, не понимая, что происходит, поскольку в браузере и в Postman возвращался нормальный JSON и только при запуске обращения к сервере через JavaScript возникала эта ошибка. Затем наткнулся на этот сервер: https://reqbin.com/post-online для проверки GET запросов. Подсунув туда URL на ngrok я получил в response HTML страницу, вместо JSON.

Странно, что при отправке запросов через Postman и другие сервера они всегда возвращали правильный JSON. Чтобы bypass ngrok browser warning нужно добавить в header строку:

'ngrok-skip-browser-warning': 'any'

Добавление нестандартного User-Agent, как указано в рекомендациях, проблему решает в https://reqbin.com/post-online, но при вызове в JavaScript возникает та-же ошибка. Рабочий JavaScript код для обхода ошибки:

&lt;script>
const url = 'https://xxx.ngrok-free.app/api/counter';
//const url = "https://jsonplaceholder.typicode.com/posts/1";
var headers = new Headers(
	{
        'User-Agent': 'PostmanRuntime/7.36.0',
        'Accept': 'application/json',
        'ngrok-skip-browser-warning': 'any',
        'Host': 'hugely-easy-pipefish.ngrok-free.app',
        'Content-Type': 'application/json'
  }
)
fetch(url, {
    method: "GET",
    headers: headers,  
    redirect: "follow"
  }
).then(response => {
    //console.log(response);
    return response.json();
  }
).then(data => {
    console.log(data);
  }  
).catch(error => {
    console.log(error.message);
  }
)
&lt;/script>

Запускать скрипт для отладки удобно в https://jsfiddle.net/. Если раскомментарить строчку console.log(response), то в консоли jsfiddle отобразится объект response со всеми свойствами и методами.

Рубрика: IT рецепты | Оставить комментарий

Скачивание данных с Yandex.Disk с помощью Python

При работе на Google Colab-е столкнулся с ситуацией, когда потребовалось скачать исходники разработанного модуля с Яндекс Диска (Yandex.Disk) на локальный диск Colab. Регистрация приложения для использования библиотеки YaDisk довольно небыстрая. Но можно сделать, если есть время. Ссылка.

Я пошел альтернативным способом, через выдирание параметров авторизации из опубликованной в Яндекс Диск ссылки. Так гораздо проще для простых реализаций.

При скачивании папки с Яндекс Диска в виде архива нужно извлечь папки. У меня все модули Python в одной папке, поэтому интересовала только корневая.

import zipfile, io, os, sys

#Извлекаем директории из архива
def getZipDirs(zip_f, isRootDirOnly = False, addPath = True):
  dirs = []
  for f in zip_f.namelist():
    zinfo = zip_f.getinfo(f)
    if zinfo.is_dir():
      if isRootDirOnly:     # Only root directories:
        # This is will work in any OS because the zip format specifies a forward slash.
        r_dir = f.split('/')
        r_dir = r_dir[0]
        if r_dir not in dirs:
          dirs.append(r_dir)
      else: # All directories:
          dirs.append(f)
  if addPath:
    for dir in dirs:
      sys.path.append(os.path.abspath('/content/' + dir + "/"))        
  return dirs

У функции скачивания файла с Yandex Disk два параметра:

  • url — путь к файлу или папке (например, https://disk.yandex.ru/d/rVVprjVVV2hS-w)
  • isZip — если передан путь к папке, то True, поскольку её содержимое вернется в Zip архиве. Если скачивание текстового файла, то False.

В этой функции архив сразу распаковывается на диск Colab и прописывается в путях для импортирования модуля.

import requests, sys

#url - путь к файлу или папке
#isZip - если передан путь к папке, то её содержимое вернется в Zip архиве
def loadTextFromYandexDisk(url: str, isZip = False):
  base_url = 'https://cloud-api.yandex.net/v1/disk/public/resources/download?'
  final_url = base_url + urlencode(dict(public_key=url))
  response = requests.get(final_url)
  download_url = response.json()['href']
  download_response = requests.get(download_url)
 
  if isZip:
    zip = zipfile.ZipFile(io.BytesIO(download_response.content))
    dirs = getZipDirs(zip, True)
    zip.extractall("/content/")
  else:  
    download_response.encoding = download_response.apparent_encoding
    return download_response.text 

Не забываем добавлять в начало кода код для автозагрузки модуля при обновлении исходников.

%load_ext autoreload
%autoreload 2 #Reload all modules (except those excluded by %aimport) every time before executing the Python code typed.
Рубрика: IT рецепты | Оставить комментарий

Соединение более 3-х силовых кабелей под клеммную колодку контактора/автомата

Иногда возникает необходимость под прижимной зажим контактора или автомата притянуть более 2-х силовых проводов. Например, в электрических котлах на каждую фазу может подключаться 3 реле (9 ступеней), соответственно, 3 провода надо завести под клеммник, что несколько противоречит требованиями ГОСТ/ПУЭ.

Если это разделка одного распределительного шкафа, то все довольно легко, достаточно использовать клеммные сборки или шины, вариантов много. Но что делать, если такую коммутацию надо сделать в серийном электроприборе, где цена лишнего компонента значительно скмзывается на розничной цене для потребителя? Каждый винтовой узел добавляет брака при сборке из-за недотяжки, разбалтывании при транспортировке, при вибрации внутри прибора и пр.

Самый простой вариант скоммутировать под одну клеммную колодку контактора/автомата три многожильных медных провода без нарушения ПУЭ/ГОСТ — использовать комбинацию концов НШВИ + НШВИ(2). При этом:

  1. П 3.4.7 ПУЭ «Присоединение двух медных жил кабеля под один винт не рекомендуется, а двух алюминиевых жил не допускается.». Материал коннектора у KBT: медь марки М1.
  2. Согласно ГОСТ 10434-82 «СОЕДИНЕНИЯ КОНТАКТНЫЕ ЭЛЕКТРИЧЕСКИЕ». 2.1.12. К каждому болту (винту) плоского вывода или к штыревому выводу рекомендуется присоединять НЕ БОЛЕЕ двух проводников, если иное не указано в стандартах или технических условиях на электротехнические устройства конкретных видов.
  3. Соответственно, оба стандарта НЕ РЕКОМЕНДУЮТ, но и НЕ ЗАПРЕЩАЮТ соединять два МЕДНЫХ кабеля.
  4. НШВИ(2) — это стандартный наконечник под два провода. Возможно, с этим и есть какие-то проблемы в части надежности такого способа соединения, но он вполне допустим.
  5. «К каждому болту (винту) плоского вывода или к штыревому выводу рекомендуется присоединять не более двух проводников». Многожильный проводник при заведении под клеммную колодку контактора, по-правилам, гильзуется, в т.ч. НШВИ. Соответственно, не запрещено вводить два НШВИ+НШВИ, хоть и не рекомендуется.
  6. При заведении вместо двух НШВИ+НШВИ двух наконечников НШВИ + НШВИ(2) под пластину клеммной колодки контактора вполне допустимо.

Однако, есть риск, что при опрессовке НШВИ и НШВИ(2) высоты опрессованных концов могут немного отличаться. При прижатии планкой может возникнуть чуть более худший контакт у одного из проводников и разогрев на больших токах в этом месте. Но это гипотеза. Она ровно также может быть при опрессовке двух концов НШВИ + НШВИ. 

Винтовое соединение, по опыту — это один из наиболее браконосных узлов при серийном производстве, поскольку его могут недотянуть, не исключено разбалтывание при транспортировке, при вибрации от работы насоса и пр. Чем меньше винтовых соединений, тем лучше.

При таком соединении 3-х проводов к каждой клемме контактора получается ВСЕГО!!! 3-и винтовых соединения по два конца НШВИ + НШВИ(2). Причем, в зависимости от типа реализации способа прижатия конца провода в контакторе, риски из-за разной толщины могут нивелироваться.

Соотвественно, такое решение:

  1. Не запрещено ГОСТ/ПУЭ, хоть и не является рекомендованным. 
  2. Минимум винтовых соединений, соответственно в разы сокращается брак.
  3. Очень дешевое. Переход на клеммные сборки — это приличный рост себестоимости готового изделия.
  4. Достаточно надежное при правильном исполнении: обжимка и затяжка.
  5. Наименее трудозатратное при сборке.
  6. Очень компактное.
  7. Проверенное годами эксплуатации. Брак из-за сборки минимальный. В ходе эксплуатции решение хорошо себя зарекомендовало при качественном контакторе, когда не экономят на меди и серебре.

Предположим, чтобы было красиво и правильно переходим на винтовые клеммные сборки или шины по одному проводу в гнездо, как полагается. Тогда:

  1. Есть 3 винта на контакторе + 3 винта на переход кабеля от контактора на клеммную сборку + 3 винта под каждый провод на одной фазе. Итого получаем: 3 + 3 + 3 * 3 фазы = 15 винтов.
  2. Кол-ва брака возрастает в несколько раз, поскольку на каждой затяжке винта увеличается риск получения нагрева.
  3. Хотя здесь нагрев будет другим. Он будет распределятся по значительно большей площади металла.
  4. Трудозатраты при сборке возрастают в 5 раз.

Казалось бы, решение оптимальное, ничему не противоречит, но нарекания при эксплуатации все-же случаются. Сам контактор при низком уровне серебра на контактах (возможно, что и при нормальном тоже) и/или при неполном соединении контактов при пониженном напряжении на катушке и пр. моментах — также разогревается.

Соответственно, имеем разогрев контакта контактора при срабатывании. Тепло от него по медной пластине передается на клеммник. К этому добавляется ещё разогрев клеммника контактора если плохо поджаты НШВИ. Эти две генерации тепла находятся близко друг от друга, увеличивая температуру разогрева, при малой площади теплоотведения.

На дорогих и качественных контакторах такого не происходит:

  • Медные проводники взяты с запасом, что позволяет рассеивать тепло.
  • На контатках достаточно серебра, чтобы контакт был хорошим и разогрев был минимальным.
  • Сам механизм прижатия вводимых кабелей обеспечивает плотное примыкание.

Чтобы нивелировать проблемы при некачественном контакторе напрашивается простое и дешевое решение — использовать повышенное сечение жилы кабеля подводимого к контактору. Например, если расчетное сечение 2,5 мм2, то использовать 4 мм2. С увеличением диаметра жилы увеличится и диаметр гильзы НШВИ. В совокупности это позволит:

  • Увеличить площадь контакта за счет большеей площади опрессованного наконечника.
  • Лучше отводить тепло от контакта контактора за счет большей площади сечения провода.
  • Распределять тепло более равномерно, чтобы не допускать перегревания изоляции.  

Клеммники на DIN рейку

Если делать более красивый вариант с клеммными сборками или шинами. Про количество винтовых соединений и роста брака из-за этого я уже написал. Однако, есть и плюсы:

  1. Если на контакторе только один НШВИ, контакт, несомненно, будет лучше из-за отсутствия перепада высот на двух наконечниках.
  2. Уменьшается риск неподжатия контактом клеммы контактора наконечника НШВИ. Это приведет к снижению разогрева.
  3. Кабель от контактора до клеммника рассеивает тепло. Его диаметр нужно брать с запасом. Например, вместо расчетных 2,5 мм2 брать 4 мм2, чтобы отводить тепло от контактора максимально эффективно.
  4. Появляется металл клеммника к которому приходит кабель от контактора, дополнительно рассеивая тепло. До него тепло вряд ли дойдет, но все-же…

Остается вопрос что делать с клеммными блоками. Если ставить качественные клеммные блоки, то их нужно много, поскольку только на клеммный блок с болтовым соединением можно завести два провода. Например, на такой https://youtu.be/XD5f-v9AjQQ?si=OS2nZ6PW_OKD-Dyi.

Остальные типы фиксиации в клеммныом блоке не позволят ввести два провода в одно гнездо. Хотя некоторые клеммники позволяют заводить по два провода с каждой стороны, но тогда, как правило, увеличивается количество винтовых соединений. Придется либо использовать НШВИ(2), либо увеличивать количество клеммных блоков, соединяя их заводской перемычкой. Итого получаем:

  • Нужны секции клемников, которые допускают установку перемычки.
  • Тогда на каждую фазу нам нужно две секции и перемычка на два клеммника.
  • Клеммник должен позволять заводить кабель 4 мм2 и 2,5 мм2, либо придется ставить два разных клеммника.
  • Соединяем два клеммника друг с другом заводской перемычкой.
  • Заводим один провод 4 мм2 от контактора.
  • Заводим 3 провода 2,5 мм2 в три оставшихся гнезда клеммника.
  • Итого на каждую фазу 6 секций клеммника и 3 перемычки на 2.
  • Ценник будет приличный.

Примерно вот так это выглядит на котле Lemax Pro Plus:

Можно использовать недорогой клеммник с болтовым соединением и гроверной шайбой для исключения разбалтывания от вибрации. Он существенно более трудозатратный при сборке, однако обеспечивает лучший контакт. Подгорать будет (если будет) более дешевый клеммник, а не контактор. Клеммник можно взять, например, такой:

Решение также не идеальное:

  1. С каждой стороны клеммы придется под болт загонять по 2 провода. Это будет также не рекомендуемым, но допустимым вариантом.
  2. Вот здесь показано, что до 2-х концов можно использовать https://youtu.be/XD5f-v9AjQQ?si=OS2nZ6PW_OKD-Dyi и это рекомендации от Феникса.
  3. Соответственно оконцовывать надо вот такими концами, поскольку U-образные не позволят обеспечить качественный контакт при накладывании друг на друга.
  4. Это, несомненно, более трудозатратный вариант при сборке, чем ввести провод в контактор и затянуть динамометрической отверткой.

Наконечники кольцевые изолированные с ПВХ манжетой

Хотя есть более компактное решение под тип наконечников нередко используемых на силовых реле. Так называемый «tab connector terminal block«. Он более «плотный» для компоновки.

Tab connector terminal block

Интересный вариант плотного клеммника с хорошей изоляцией, т.е. минимальным риском для монтажника, на DIN рейку с возможностью установки перемычки CTC4U (каталог):

Под вилки РПИ-М. Вроде такой:

Есть ещё вот такие power distubution blocks http://www.blox-electric.com/list/post/2385969/, но таких надо 3 шт. на каждую фазу и стоят они в районе 250 руб. При габаритах 66x27x46 займет немало места.

Насколько значительно улучшается качество контакта/снижается уровень брака в процессе эксплуатции при использовании дополнительных клеммных блоков сказать сложно. Но точно увеличивается себестоимость и увеличивается место под дополнительные компоненты.

Рубрика: Лайфхаки | Оставить комментарий

Проверка в AimyLogic создания дубликата записи смарт-процесса в Битрикс (REST API)

В продолжение другой статьи по теме интеграции Aimylogic c Битрикс рассмотрим как реализовать следующий алгоритм:

  • Клиент звонит и попадает на голосовой бот. Оставляет заявку в ServiceDesk (например, Битрикс).
  • Время обработки сервисным инженером заявки, созданной голосовым ботом, например, 2 дня.
  • Нетерпеливый пользователь, не дождавшись звонка от инженера, повторно звонит и попадает на голосового бота Aimylogic.
  • Голосовой бот должен посмотреть, за последнее время были ли созданы заявки от пользователя с того-же номера.
  • Если такие заявки есть, то голосовой бот должен успокоить клиента, сообщив, что в системе уже есть заявка. Звонок связан с новой проблемой или с ранее зарегистрированной.
  • Если клиент озвучит, что проблема другая, то должна создастся новая заявка.

Получение контакта клиента в Bitrix через REST API

Итак, Aimylogic определил с какого номера пришел звонок и ему нужно по этому номеру определить сервисные заявки для которых указан контакт с этим номером. Для определения есть ли клиент с указанным номером $client_phone в базе контактов Битриска используется следующий запрос:

https://xxxxxxxx.ru/rest/177/xxxxxxxxxxxx/crm.duplicate.findbycomm.json

{
"entity_type": "CONTACT",
"type": "PHONE",
"values": [ "$client_phone" ]
}

В ответе берем переменную $httpResponse.result в которой передается ID контакта.

Напомню, что добавляли смарт-процесс используя следующий POST запрос:

https://xxxxxxxxxxx.ru/rest/177/xxxxxxxxxxxxxxx/crm.item.add.json

{
"entityTypeId": "158",
"fields": {
"SOURCE_ID": "14",
"contactId": "$contact_id",
"ufCrm3_1680255422": "$equipment", 
"ufCrm3_1680254883": "$client_city",
"ufCrm3_1680261235": "$question",
"ufCrm3_1687168305": "$mobilePhone", 
"ufCrm3_1680261458": "$productiondate",
"ufCrm3_1697115862": "$equipment_state"
}
}

Соответственно, нам надо получить список смарт-процессов «сервисная заявка» у которых в поле «contactId» прописан полученный ранее ID контакта. Протестируем запрос в Postman на примере контакта с известным номером мобильного телефона. Получим несколько ID по номеру:

{
    "result": {
        "CONTACT": [
            14116,
            14118
        ]
    },
    "time": {
        "start": 1699808317.849979,
        "finish": 1699808317.890918,
        "duration": 0.0409390926361084,
        "processing": 0.004055976867675781,
        "date_start": "2023-11-12T19:58:37+03:00",
        "date_finish": "2023-11-12T19:58:37+03:00"
    }
}

Видим, что с указанным номером в Битриксе есть два контакта. Контакты были созданы через разные источники. Например, через форму обратной связи и обращение через голосового бота. Считаем, что это аномалия, которую надо устранить дополнительной проверкой, чтобы исключить дублирование при создании контактов с одним номером телефона.

Посмотрим как выглядит ответ для номера у которого точно один контакт в Битриксе:

{
    "result": {
        "CONTACT": [
            14111
        ]
    },
    "time": {
        "start": 1699809721.426188,
        "finish": 1699809721.46897,
        "duration": 0.04278206825256348,
        "processing": 0.005627155303955078,
        "date_start": "2023-11-12T20:22:01+03:00",
        "date_finish": "2023-11-12T20:22:01+03:00"
    }
}

Тот-же массив «CONTACT», т.е. нам достаточно в Aimylogic взять первый элемент в этом массиве.

function getContact(contacts) {
    
    try {
        $obj = JSON.parse(contacts);

        if ("CONTACT" in $obj) {
            if ($obj.CONTACT.length > 0)
            {
                $contact = $obj.CONTACT[0];
            }
        }
    } catch (e) {
        return -1   
    }
    return $contact
}

$contact = getContact($contacts)

Получение сервисной заявки (смарт-процесс) клиента в Bitrix через REST API

Чтобы получить сервисную заявку к которой привязан найденный контакт воспользуемся методом crm.item.list:

https://xxxxxxxxx.ru/rest/177/xxxxxxxxxxxxx/crm.item.list.json 

{
    "entityTypeId": 158,
    "filter": {
        "=contactId": "14111"
    }
}

После выполнения запроса к Битриксу получаем JSON в котором возвращается найденная сервисная заявка со всеми нужными нам полями. Полей много, поэтому заменил на …….

{
    "result": {
        "items": [
            {
                "id": 2214,
                "title": "Сервисная заявка #2214",
                "contactId": 14111,
........
                "entityTypeId": 158
            }
        ]
    },
    "total": 1,
    "time": {
        "start": 1699811918.669397,
        "finish": 1699811918.71892,
        "duration": 0.04952287673950195,
        "processing": 0.021754980087280273,
        "date_start": "2023-11-12T20:58:38+03:00",
        "date_finish": "2023-11-12T20:58:38+03:00"
    }
}

Чтобы сократить нагрузку на сервер и сократить трафик, можно выбирать в запросе только нужные поля:

{
    "entityTypeId": 158,
    "select": ["contactId", "stageId", "id", "createdTime"],
    "filter": {
        "=contactId": "14111" 
    }
}

Соответственно, создаем в AimyLogic HTTP запрос POST к методу crm.item.list.json и в body прописываем:

{
    "entityTypeId": 158,
    "filter": {
        "=contactId": "$contact"
    }
}

Результат $httpResponse.result помещаем в переменную $items. И далее обрабатываем полученный JSON следующим образом:

function getItem(items) {
    try {
        $obj = JSON.parse(items);

        if ($obj.items) {
            if ($obj.items.length > 0)
            {
                $item = $obj.items[0];
            }
        }
    } catch (e) {
        return -1
    }
    return $item
}

$item = getItem($items);
$item_created = -1;
if ($item != -1) {
    $item_id = $item.id;
    $item_created = $item.createdTime;
}

Все отрабатывает нормально. Получаем сервисную заявку и берем дату создания.

Определение срока давности сервисной заявки

Осталось найти разность между моментом, когда клиент позвонил повторно и датой ранее созданной сервисной заявки:

function getDateDifference(isoDateStr)
{
    try {
        $date1 = new Date(isoDateStr);
        $date2 = new Date();
        return ($date2.getTime() - $date1.getTime()) / (1000 * 3600 * 24); 
    } catch (e) {
        return Number.MAX_VALUE;
    }
}

if ($item_created != -1) {
    $daysdiff = getDateDifference($item_created);
}

Блоки кода целесообразно вставлять в блоки условий перед проверкой условия. Т.е. проверяем, что результат на выходе функции равен -1 и тогда отправляем на сообщение об ошибке, точнее отправляем клиента по стандартному маршруту, когда бот будет задавать ему перечень стандартных вопросов. По ветке else отправляем на следующий этап обработки.

Можно реализовать проверку на дату непосредственно в запросе на поиск нужных сервисных заявок (смарт-процессов). Для этого нам надо отфильтровать заявки у которых поле createdTime старше текущей даты не более, чем на n дней. Т.е. берем текущую дату, вычитаем n дней и смотрим все записи у которых createdTime больше полученной даты.

{
    "filter": {
        ">createdTime":"2020-03-19T02:00:00+02:00"
    }
} 

Пример финального запроса будет выглядеть следующим образом:

https://xxxxxxxxx.ru/rest/177/xxxxxxxxxxxxx/crm.item.list.json 

{
    "entityTypeId": 158,
    "select": ["contactId", "stageId", "id", "createdTime"],
    "filter": {
        "=contactId": "14111",
        ">createdTime": "2023-11-11T15:08:13+03:00" 
    }
}

Итак, после обработки полученной даты создания сервисной заявки получили разность дат в днях и можно принимать решения, озвучить клиенту, что он недавно создал заявку и она в обработке или позволить создать новую заявку. Можно дополнительно уточнить у клиента, обращение по старой заявке или какая-то новая проблема.

В этот запрос надо ещё добавить поиск только тех заявок у которых статус stageId удовлетворяет необходимым. Например, если stageId — SUCCESS, при этом дата создания заявки менее n дней назад, то:

  • Либо что-то пошло не так и у клиента возникли проблемы с этой заявкой. Тогда нужно спросить его об этом и сменить статус (переоткрыть), чтобы доделать.
  • Либо у клиента действительно все решено, но возник новый вопрос и надо создать новую заявку.

Поскольку здесь действия вариативные, нельзя включать в предфильтрацию stageId и фильтровать, например, только заявки у которых состояние SUCCESS.

Определение даты создания самой свежей сервисной заявки

Теперь нужно учесть момент, что у клиента с одним и тем-же номером телефона, но, например, несколькими контактами в Битриксе, может быть несколько сервисных заявок созданных в разное время. Соответственно, нужно пройтись по всем его заявкам и найти самую свежую. Гипотетически, если заявка была создана относительно недавно (в пределах 2-3 дней), то пользователь перезванивает по ней, поскольку сервиcные инженеры ещё не дошли.

После выполнения HTTP запроса на получение списка контактов, конвертируем в массив $contacts.

function getContact(contacts) {
    
    try {
        $obj = JSON.parse(contacts);

        if ("CONTACT" in $obj) {
            if ($obj.CONTACT.length > 0)
            {
                return $obj.CONTACT;
            }
        }
    } catch (e) {
        return -1   
    }
}

$contacts = getContact($contacts);

Склеиваем элементы массива в строку для передачи в HTTP POST запрос. Из id контактов должно получится что-то вроде: [«14116», «14118», «14142»]

//Функция склеивает массив в строку для POST запроса. Например, ["14116", "14118", "14142"]
function getContactsList(contacts)
{
    try {
        return "[\"" + contacts.join("\", \"") + "\"]";
    } catch (e)
    {
        return "[]";
    }
}

$contacts_str = getContactsList($contacts)

Теперь подаем результат в HTTP POST запрос Aimylogic.

{
    "entityTypeId": 158,
    "select": ["contactId", "stageId", "id", "createdTime"],
    "filter": {
        "=contactId": "$contacts_str",
        ">createdTime": "$minStartDate" 
    }
}

После выполнения запроса в переменной $items = $httpResponse.result будет JSON с массивом ВСЕХ сервисных заявок, которые привязаны к ВСЕМ номеру телефона клиента.

Ну и далее перебираем все даты создания сервисных заявок и находим сервисную заявку с наиболее близкой датой.

function getNewestItem(items) {
    try {
        $obj = JSON.parse(items);
        $lastDate = new Date(1975, 10, 16); //Инициализируем произвольной датой в прошлом. 

        $newest_item = -1;
        if ($obj.items) {
            if ($obj.items.length > 0)
            {
                for ($i = 0; $i &lt; $obj.items.length; $i++) {
                    try
                    {
                        $item = $obj.items[$i];
                        $item_currentDate = new Date($item.createdTime);
                        if ($item_currentDate > $lastDate)
                        {
                            $lastDate = $item_currentDate;
                            $newest_item = $item;
                        }
                    } catch (e) {                    
                    } 
                }
                return $newest_item;
            }
        }
        return -1;
    } catch (e) {
        return -1;
    }
}

$item = getNewestItem($items)

Высчитываем разность между датами, как делали это раньше. Для диагностики при отладке можно вывести информацию в блоке «Синтез речи»:

if ($item != -1) {
    $item_created = $item.createdTime;
    $date1 = new Date($item_created);
    $date2 = new Date();
    $daysdiff = getDateDifference($item_created);
}

Далее алгоритм Aimylogic затачиваем под проверку разницы дат и дополнительным вопросам, а уверен ли пользователь, что это не повторная заявка.

Рубрика: IT рецепты | Оставить комментарий

Мамы, дети и исключение отца

Что меня всегда угнетало в некоторых женщинах — это патологическая обидчивость и потом желание наказать безмолвием. Ну типа, прочувствуй свою вину сам. 🙂 Я слова не скажу, чтобы ты сам понял, какой-же ты негодяй. Не важно, что мужчина нередко не особо понимает на что именно взъелась супруга. С т.з. женщины это никак не влияет на воспитательный процесс. 🙂

В общем-то до какого-то возраста меня это задевало. Потом, уже после развода, особо перестало напрягать, хотя недоумение осталось.

Крайнее проявление такого подхода — приучение детей, дочерей в первую очередь, ровно к такому-же деструктивному подходу. Подход реально патологический, поскольку он не приводит к какой-то конструктивному диалогу, чтобы разобраться и найти компромисс. Это продавливание своей точки зрения психологически давящим способом — изоляцией.

Изгнание из социума в древности было одним из самых серьезных наказаний. Без общины человек с высокой вероятностью был обречен на смерть. Получается женщина выбирает самый антигуманный и наиболее действенный, с её точки зрения, подход, чтобы что-то продавить. Конечно, искать компромисс — куда более энергетически сложная задача.

В стае волков исключение особи из стаи фактически означало, что изгнанный будет питаться чем-то вроде лягушек. Загнать крупную добычу без соплеменников не получится. Нечто подобное было и у человека. Лиши его крова, огня и поддержки группы и выживать будет гораздо сложнее.

В расстановках по Берту Хеллингеру есть закон принадлежности, которому, по опыту автора, следуют системы. При его принудительном нарушении система пытается компенсировать исключение человека, перенося его линию поведения (или что-то, что визуально нами считывается как линия поведения) на другого с наименьшими связами с системой. Это, как правило, всегда дети, поскольку по закону порядка они наименее важны для системы, т.к. пришли в неё последними.

На обучении по бизнес-расстановкам Олег Вайнберг приводил такой пример. Если из середины стены (система) извлечь кирпич (элемент системы — человек), то она становится менее прочной. Система стремится залатать брешь, поэтому берет кирпич с самого верха стены (ребенок), у которого связи с другими элементами системы минимальны и ставит его на место извлеченного элемента. Своебразное самолечение. 🙂

Грубо говоря, женщина обиделись на мужа и решила стереть из своей памяти и памяти ребенка любое воспоминание о нем, оборвать все связи. Она начинает целенаправлено исключать любые упоминания и наказывать ребенка за то, что он вспоминает отца. Наказание может выражаться вот в таком вот уничижающем молчании, которое дает понять ребенку, что он совершил нечто постыдное и за это несет наказание в виде симуляции исключения.

С т.з. женщины, видимо, этот подход является целительным для ребенка. Ребенок же, залатывая брешь в системе (не знаю каким образом это происходит, психологи установили эмпирическим путем и лишь рассказывают гипотезы), будет неосознанно копировать элементы поведения исключенного. Ну т.е. хотите, чтобы ребенок повторял отца — исключите его из семьи, убрав всякое упоминание, уничтожив семейные фото, видео и пр.

В системе важно уважение ко всем, кто появился там ранее. «Можно осуждать грех человека, но нельзя осуждать человека». Человек не намеренно и осознанно делает какие-то действия, а из каких-то лучших, с его точки зрения и точки зрения системы, побуждений. То, что нам они кажутся худшими — это лишь наш угол зрения с которого мы обозреваем проявление. Если посмотреть под другим углом, то все может оказаться совершенно иным.

Например, сын напиваясь может таким образом демонстрировать любовь к отцу, который рано умер из-за церроза печени и был исключен из системы, поскольку маме думалось, что он намеренно себя так вел, чтобы её позлить. Исключен — это значит любые упоминания о нем были постыдными и карались соответственно. Причем, с т.з. женщины, мысль может быть прямолинейно-здравая, если перестать упоминать об отце-алкоголике, сын не пойдет его путем. На самом деле, все может быть с точностью до наоборот.

К чему я это все вспомнил. Завтра мой день рождения. Бывшая старательно вычеркнивала меня из памяти ребенка с момента нашего расставания. За все это время дочь ни разу не поздравила с ДР открыткой или SMS-кой. Хотя до развода отношения были хорошие. Я старался каждый раз на день её рождения отправить открытку, SMS маме и отправлял открытки почти из всех стран, где бывал. Понятно, что, скорее всего, ни одна открытка до дочери не дошла, перехваченная мамой. Я не забываю дочь, хотя прошло уже много лет с момента последнего общения. Влияние матери на ребенка очень сильное.

Когда лишаешься чего-то привычного, но ценного, начинаешь понимать, что не ценил и насколько это важно. Забавно, но сейчас самый душевный подарок на мой ДР была бы SMS с тексом, вроде: «Пап, с Днем Рождения». 🙂 Как «мало» надо для счастья.

Я бывал на разных психологических тренингах. На них, частенько, немало взрослых детей, которые до сих пор пытаются «переварить» подобную изоляцию родителями кого-то из членов семьи. Не обязательно это отец или мать, это могут быть какие-то близкие родственники: бабушки, дедушки, сестры, братья и пр., когда их исключили из-за несоотвествующего шабону родителей поведения. Грустно все это… Хотя, может в этом и состоит смысл жизни, чтобы сначала потерять, потом обрести вновь. 🙂

В общем, если нужно, чтобы ребенок немало времени потратил у психологов — исключите кого-то из близких и результат будет гарантирован. 🙁

Рубрика: О жизни, Отношения, Психология, Семья | Оставить комментарий