This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
|
язык_программирования_python [2026/07/11 09:13] val [request_to_agent.py] |
язык_программирования_python [2026/07/17 06:25] (current) val [ari_chat.py] |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====== Язык программирования Python ====== | ====== Язык программирования Python ====== | ||
| + | |||
| + | * [[Все что должен знать DevOps про Python]] | ||
| * [[https://habr.com/ru/articles/277679/|Пишем shell скрипты на Python и можно ли заменить им Bash]] | * [[https://habr.com/ru/articles/277679/|Пишем shell скрипты на Python и можно ли заменить им Bash]] | ||
| Line 400: | Line 402: | ||
| ==== wav_and_ogg.py ==== | ==== wav_and_ogg.py ==== | ||
| + | |||
| + | * [[Перекодировка звука#Перекодировка в ogg формат]] | ||
| + | |||
| <code> | <code> | ||
| + | $ pip install pydub pip #audioop-lts | ||
| + | </code><code> | ||
| import sys | import sys | ||
| from pydub import AudioSegment | from pydub import AudioSegment | ||
| Line 422: | Line 429: | ||
| # wav_to_ogg(target_file) | # wav_to_ogg(target_file) | ||
| # ogg_to_wav(target_file) | # ogg_to_wav(target_file) | ||
| + | </code><code> | ||
| + | $ python wav_and_ogg.py speech | ||
| </code> | </code> | ||
| ==== speech_and_text.py ==== | ==== speech_and_text.py ==== | ||
| + | |||
| + | * [[Yandex AI]] | ||
| + | |||
| <code> | <code> | ||
| import os | import os | ||
| Line 430: | Line 442: | ||
| import requests | import requests | ||
| - | API_KEY = os.getenv("API_KEY") | + | API_KEY = os.getenv("YANDEX_API_KEY") |
| - | FOLDER_ID = os.getenv("FOLDER_ID") | + | FOLDER_ID = os.getenv("YANDEX_FOLDER_ID") |
| def speech_to_text(file_path): | def speech_to_text(file_path): | ||
| Line 495: | Line 507: | ||
| sdk = AIStudio( | sdk = AIStudio( | ||
| - | folder_id=os.getenv("FOLDER_ID"), | + | folder_id=os.getenv("YANDEX_FOLDER_ID"), |
| - | auth=os.getenv("API_KEY") | + | auth=os.getenv("YANDEX_API_KEY") |
| ) | ) | ||
| messages = [{"role": "system", "text": system_prompt}] | messages = [{"role": "system", "text": system_prompt}] | ||
| Line 538: | Line 550: | ||
| chat_context.append({"role": "user", "text": user_request}) | chat_context.append({"role": "user", "text": user_request}) | ||
| chat_context.append({"role": "assistant", "text": reply_text}) | chat_context.append({"role": "assistant", "text": reply_text}) | ||
| + | </code> | ||
| + | |||
| + | ==== ari_rec.py ==== | ||
| + | <code> | ||
| + | root# | ||
| + | mkdir -p /var/spool/asterisk/recording/ | ||
| + | chown -R asterisk:asterisk /var/spool/asterisk/recording/ | ||
| + | chmod -R 775 /var/spool/asterisk/recording/ | ||
| + | |||
| + | $ pip install websocket-client requests | ||
| + | </code><code> | ||
| + | import json | ||
| + | import requests | ||
| + | import websocket | ||
| + | |||
| + | ARI_USER = "asterisk" | ||
| + | ARI_PASS = "asterisk" | ||
| + | BASE_URL = "http://localhost:8088/ari" | ||
| + | AUTH = (ARI_USER, ARI_PASS) | ||
| + | |||
| + | def detect_speech(channel_id): | ||
| + | print(f"Запускаем TALK_DETECT для канала {channel_id}...") | ||
| + | talk_url = f"{BASE_URL}/channels/{channel_id}/variable" | ||
| + | |||
| + | # 2000 мс тишины для завершения фразы, 500 мс речи для начала | ||
| + | talk_payload = { | ||
| + | "variable": "TALK_DETECT(set)", | ||
| + | "value": "2000,500" | ||
| + | } | ||
| + | |||
| + | response = requests.post(talk_url, json=talk_payload, auth=AUTH) | ||
| + | print(f"Статус-код ответа: {response.status_code}") | ||
| + | print(f"Текст ответа: {response.text}") | ||
| + | |||
| + | def undetect_speech(channel_id): | ||
| + | print(f"Отключаем TALK_DETECT для канала {channel_id}...") | ||
| + | talk_url = f"{BASE_URL}/channels/{channel_id}/variable" | ||
| + | |||
| + | talk_payload = { | ||
| + | "variable": "TALK_DETECT(set)", | ||
| + | "value": "remove" | ||
| + | } | ||
| + | |||
| + | response = requests.post(talk_url, json=talk_payload, auth=AUTH) | ||
| + | print(f"Статус-код ответа: {response.status_code}") | ||
| + | print(f"Текст ответа: {response.text}") | ||
| + | |||
| + | def start_recording(channel_id): | ||
| + | print(f"Включаем запись для канала {channel_id}") | ||
| + | record_url = f"{BASE_URL}/channels/{channel_id}/record" | ||
| + | recording_name = f"rec_{channel_id}" | ||
| + | record_payload = { | ||
| + | "name": recording_name, | ||
| + | "format": "wav", | ||
| + | "ifExists": "overwrite", | ||
| + | #"maxDuration": 29, | ||
| + | #"terminateOn": "#", | ||
| + | #"beep": True, | ||
| + | } | ||
| + | requests.post(record_url, params=record_payload, auth=AUTH) | ||
| + | |||
| + | def stop_recording(channel_id): | ||
| + | print(f"Останавливаем запись для канала {channel_id}") | ||
| + | recording_name = f"rec_{channel_id}" | ||
| + | stop_url = f"{BASE_URL}/recordings/live/{recording_name}/stop" | ||
| + | requests.post(stop_url, auth=AUTH) | ||
| + | |||
| + | def on_message(wsapp, message): | ||
| + | event = json.loads(message) | ||
| + | event_type = event.get("type") | ||
| + | |||
| + | # Сценарий 1: Звонок поступил -> Отвечаем и воспроизводим beep | ||
| + | if event_type == "StasisStart": | ||
| + | channel_id = event.get("channel", {}).get("id") | ||
| + | print(f"Отвечаем на звонок {channel_id}") | ||
| + | requests.post(f"{BASE_URL}/channels/{channel_id}/answer", auth=AUTH) | ||
| + | |||
| + | #start_recording(channel_id) | ||
| + | # or | ||
| + | detect_speech(channel_id) | ||
| + | |||
| + | # Сценарий N | ||
| + | elif event_type == "ChannelTalkingStarted": | ||
| + | channel_id = event.get("channel", {}).get("id") | ||
| + | print(f"Обнаружена речь в канале {channel_id}! Начинаем запись...") | ||
| + | start_recording(channel_id) | ||
| + | |||
| + | # Сценарий N | ||
| + | elif event_type == "ChannelTalkingFinished": | ||
| + | channel_id = event.get("channel", {}).get("id") | ||
| + | print(f"Обнаружено молчание в канале {channel_id}") | ||
| + | stop_recording(channel_id) | ||
| + | undetect_speech(channel_id) | ||
| + | |||
| + | # Сценарий N: Запись завершилась -> Проигрываем её обратно | ||
| + | elif event_type == "RecordingFinished": | ||
| + | recording_data = event.get("recording", {}) | ||
| + | recording_name = recording_data.get("name") | ||
| + | target_uri = recording_data.get("target_uri", "") | ||
| + | channel_id = target_uri.split("channel:")[1] | ||
| + | |||
| + | print(f"Запись {recording_name} готова! Проигрываем её обратно в канал {channel_id}...") | ||
| + | play_url = f"{BASE_URL}/channels/{channel_id}/play" | ||
| + | play_payload = { | ||
| + | "media": f"recording:{recording_name}" | ||
| + | } | ||
| + | response = requests.post(play_url, params=play_payload, auth=AUTH) | ||
| + | print("Ответ ARI на воспроизведение:", response.status_code) | ||
| + | |||
| + | # Сценарий N: Воспроизведение завершено (бип или записанный голос) | ||
| + | elif event_type == "PlaybackFinished": | ||
| + | playback_data = event.get("playback", {}) | ||
| + | media_uri = playback_data.get("media_uri", "") | ||
| + | target_uri = playback_data.get('target_uri', '') | ||
| + | channel_id = target_uri.split("channel:")[1] | ||
| + | |||
| + | #start_recording(channel_id) | ||
| + | # or | ||
| + | detect_speech(channel_id) | ||
| + | |||
| + | # Сценарий N: Абонент повесил трубку -> Удаляем файл | ||
| + | elif event_type == "StasisEnd": | ||
| + | channel_id = event.get("channel", {}).get("id") | ||
| + | recording_name = f"rec_{channel_id}" | ||
| + | print(f"Звонок {channel_id} завершен абонентом. Удаляем файл записи...") | ||
| + | delete_url = f"{BASE_URL}/recordings/stored/{recording_name}" | ||
| + | requests.delete(delete_url, auth=AUTH) | ||
| + | |||
| + | def on_error(ws, error): | ||
| + | print(f"Произошла ошибка: {error}") | ||
| + | |||
| + | wsapp = websocket.WebSocketApp( | ||
| + | f"ws://localhost:8088/ari/events?api_key={ARI_USER}:{ARI_PASS}&app=my-first-app", | ||
| + | on_message=on_message, | ||
| + | on_error=on_error | ||
| + | ) | ||
| + | wsapp.run_forever() | ||
| </code> | </code> | ||
| Line 556: | Line 705: | ||
| SYSTEM_PROMPT = ( | SYSTEM_PROMPT = ( | ||
| "Ты — ассистент.\n" | "Ты — ассистент.\n" | ||
| - | "Отвечай кратко, проси продолжить диалог.\n" | + | "Отвечай кратко.\n" |
| + | "Не спрашивай когда напомнить.\n" | ||
| ) | ) | ||
| + | COMPRESS_PROMPT = ( | ||
| + | "Ты — инструмент оптимизации контекста. Проанализируй историю диалога. " | ||
| + | "Удали выполненные дела, приветствия и воду. Сформулируй ОДНО лаконичное " | ||
| + | "сообщение от лица пользователя, в котором отражена только актуальная суть " | ||
| + | "и невыполненные задачи/оставшиеся вопросы на текущий момент." | ||
| + | ) | ||
| # Структура: {channel_id: [{"role": "user", "text": "..."}, {"role": "assistant", "text": "..."}]} | # Структура: {channel_id: [{"role": "user", "text": "..."}, {"role": "assistant", "text": "..."}]} | ||
| channel_contexts = {} | channel_contexts = {} | ||
| + | |||
| + | channel_caller_numbers = {} | ||
| def get_channel_context(channel_id): | def get_channel_context(channel_id): | ||
| Line 573: | Line 731: | ||
| context.append({"role": "assistant", "text": agent_response}) | context.append({"role": "assistant", "text": agent_response}) | ||
| return context | return context | ||
| + | |||
| + | def load_context(caller_number): | ||
| + | file_path = f"{REC_PATH}{caller_number}.json" | ||
| + | if os.path.exists(file_path): | ||
| + | with open(file_path, 'r', encoding='utf-8') as f: | ||
| + | return json.load(f) | ||
| + | return [] | ||
| + | |||
| + | def save_context(caller_number, context): | ||
| + | file_path = f"{REC_PATH}{caller_number}.json" | ||
| + | with open(file_path, 'w', encoding='utf-8') as f: | ||
| + | json.dump(context, f, ensure_ascii=False, indent=2) | ||
| def start_recording(channel_id): | def start_recording(channel_id): | ||
| - | print(f"Включаем запись для канала {channel_id} на 5 секунд...") | + | print(f"Включаем запись для канала {channel_id} на 29 секунд...") |
| record_url = f"{BASE_URL}/channels/{channel_id}/record" | record_url = f"{BASE_URL}/channels/{channel_id}/record" | ||
| recording_name = f"rec_{channel_id}" | recording_name = f"rec_{channel_id}" | ||
| Line 581: | Line 751: | ||
| "name": recording_name, | "name": recording_name, | ||
| "format": "wav", | "format": "wav", | ||
| - | "maxDuration": 5, | + | "ifExists": "overwrite", |
| + | "maxDuration": 29, | ||
| "terminateOn": "#", | "terminateOn": "#", | ||
| - | "ifExists": "overwrite" | + | "beep": True, |
| } | } | ||
| requests.post(record_url, params=record_payload, auth=AUTH) | requests.post(record_url, params=record_payload, auth=AUTH) | ||
| + | |||
| + | def get_caller_number(channel_id): | ||
| + | url = f"{BASE_URL}/channels/{channel_id}" | ||
| + | response = requests.get(url, auth=AUTH, timeout=5) | ||
| + | channel_info = response.json() | ||
| + | caller_number = channel_info.get("caller", {}).get("number") | ||
| + | return caller_number | ||
| def on_message(wsapp, message): | def on_message(wsapp, message): | ||
| Line 591: | Line 769: | ||
| event_type = event.get("type") | event_type = event.get("type") | ||
| - | # Сценарий 1: Звонок поступил -> Отвечаем и включаем запись | + | # Сценарий 1: Звонок поступил |
| if event_type == "StasisStart": | if event_type == "StasisStart": | ||
| channel_id = event.get("channel", {}).get("id") | channel_id = event.get("channel", {}).get("id") | ||
| Line 598: | Line 776: | ||
| requests.post(f"{BASE_URL}/channels/{channel_id}/answer", auth=AUTH) | requests.post(f"{BASE_URL}/channels/{channel_id}/answer", auth=AUTH) | ||
| + | channel_caller_numbers[channel_id]=get_caller_number(channel_id) | ||
| + | channel_contexts[channel_id] = load_context(channel_caller_numbers[channel_id]) | ||
| start_recording(channel_id) | start_recording(channel_id) | ||
| Line 606: | Line 786: | ||
| target_uri = recording_data.get("target_uri", "") | target_uri = recording_data.get("target_uri", "") | ||
| channel_id = target_uri.split("channel:")[1] | channel_id = target_uri.split("channel:")[1] | ||
| + | |||
| + | caller_number = channel_caller_numbers[channel_id] | ||
| + | print(f"Номер звонящего в RecordingFinished: {caller_number}") | ||
| import wav_and_ogg | import wav_and_ogg | ||
| Line 612: | Line 795: | ||
| user_request = speech_and_text.speech_to_text(f"{REC_PATH}{recording_name}.ogg") | user_request = speech_and_text.speech_to_text(f"{REC_PATH}{recording_name}.ogg") | ||
| - | print(user_request) | + | print(f"user_request: {user_request}") |
| - | import request_to_agent | + | if user_request: |
| - | context = get_channel_context(channel_id) | + | context = get_channel_context(channel_id) |
| - | agent_response = request_to_agent.request_to_agent(user_request, SYSTEM_PROMPT, context) | + | |
| - | print(agent_response) | + | |
| - | #agent_response = user_request | + | import request_to_agent |
| + | agent_response = request_to_agent.request_to_agent(user_request, SYSTEM_PROMPT, context) | ||
| + | #agent_response = user_request | ||
| - | update_channel_context(channel_id, user_request, agent_response) | + | print(agent_response) |
| - | ''' | + | update_channel_context(channel_id, user_request, agent_response) |
| - | print("Текущий контекст:") | + | |
| - | from pprint import pprint | + | |
| - | pprint(channel_contexts) | + | |
| - | ''' | + | |
| - | speech_and_text.text_to_speech(agent_response,f"{REC_PATH}{recording_name}.response.ogg") | + | print("Текущий контекст:") |
| - | wav_and_ogg.ogg_to_wav(f"{REC_PATH}{recording_name}.response") | + | from pprint import pprint |
| + | pprint(channel_contexts[channel_id]) | ||
| - | print(f"Запись {recording_name} готова! Проигрываем её обратно в канал {channel_id}...") | + | speech_and_text.text_to_speech(agent_response,f"{REC_PATH}{recording_name}.response.ogg") |
| - | play_url = f"{BASE_URL}/channels/{channel_id}/play" | + | wav_and_ogg.ogg_to_wav(f"{REC_PATH}{recording_name}.response") |
| - | play_payload = { | + | |
| - | #"media": f"recording:{recording_name}" | + | print(f"Запись {recording_name} готова! Проигрываем её обратно в канал {channel_id}...") |
| - | "media": f"recording:{recording_name}.response" | + | play_url = f"{BASE_URL}/channels/{channel_id}/play" |
| - | } | + | play_payload = { |
| - | response = requests.post(play_url, params=play_payload, auth=AUTH) | + | #"media": f"recording:{recording_name}" |
| - | print("Ответ ARI на воспроизведение:", response.status_code) | + | "media": f"recording:{recording_name}.response" |
| + | } | ||
| + | response = requests.post(play_url, params=play_payload, auth=AUTH) | ||
| + | print("Ответ ARI на воспроизведение:", response.status_code) | ||
| - | # Сценарий 3: Воспроизведение завершено -> Возвращаемся в режим записи... | + | # Сценарий 3: Воспроизведение завершено |
| elif event_type == "PlaybackFinished": | elif event_type == "PlaybackFinished": | ||
| playback_data = event.get("playback", {}) | playback_data = event.get("playback", {}) | ||
| + | media_uri = playback_data.get("media_uri", "") | ||
| target_uri = playback_data.get('target_uri', '') | target_uri = playback_data.get('target_uri', '') | ||
| channel_id = target_uri.split("channel:")[1] | channel_id = target_uri.split("channel:")[1] | ||
| - | print(f"Воспроизведение завершено. Возвращаемся в режим записи...") | ||
| start_recording(channel_id) | start_recording(channel_id) | ||
| - | ''' | + | # Сценарий 4: Абонент повесил трубку |
| - | print(f"Вешаем трубку для channel_id {channel_id}") | + | |
| - | hangup_url = f"{BASE_URL}/channels/{channel_id}" | + | |
| - | requests.delete(hangup_url, auth=AUTH) | + | |
| - | ''' | + | |
| elif event_type == "StasisEnd": | elif event_type == "StasisEnd": | ||
| channel_id = event.get("channel", {}).get("id") | channel_id = event.get("channel", {}).get("id") | ||
| - | print(f"Звонок завершен пользователем (Канал {channel_id}). Очистка ресурсов...") | + | |
| + | context = get_channel_context(channel_id) | ||
| + | caller_number = channel_caller_numbers[channel_id] | ||
| + | print(f"Номер звонящего в StasisEnd: номер {caller_number} канал {channel_id}") | ||
| + | |||
| + | print(f"Сжатие контекста") | ||
| + | |||
| + | import request_to_agent | ||
| + | agent_response = request_to_agent.request_to_agent("Сожми контекст", COMPRESS_PROMPT, context) | ||
| + | print(agent_response) | ||
| + | context = [{"role": "user", "text": agent_response}] | ||
| + | |||
| + | print(f"Сохранение контекста и удаление медиа файлов") | ||
| + | |||
| + | save_context(caller_number,context) | ||
| import glob | import glob | ||
| recording_name = f"rec_{channel_id}" | recording_name = f"rec_{channel_id}" | ||