| import re |
| import os |
| import numpy as np |
| import requests |
| import httpx |
| import json |
| import pandas as pd |
|
|
| typedf = pd.read_excel('types.xlsx') |
| verbose = True |
| |
| |
| level = 'Тип обращения' |
| level = 'Сектор' |
| level = 'Продукт' |
| level = 'Проблема' |
| level = 'Проблема' |
|
|
| def getType(str, level): |
| type = '' |
| pattern = rf'{level}: ([^>]+)(?: >|$)' |
| m = re.search(pattern, str) |
| if m: |
| type = m.group(1).strip() |
| return type |
|
|
| def getQuestionAnswer(str) : |
| q = '' |
| a = '' |
| l = str.split('>') |
| if len(l) == 5 : |
| q = l[4][1 :] |
| x = q.find(':') |
| a = q[x + 2 :] |
| q = q[: x] |
|
|
| return q, a |
|
|
| types = [] |
| sectors = [] |
| products = {} |
| problems = {} |
| for index, row in typedf.iterrows() : |
| text = row['Путь до вершины'] |
| text = str(text) |
| if text != '' : |
| apptype = getType(text, 'Тип обращения') |
| sector = getType(text, 'Сектор') |
| product = getType(text, 'Продукт') |
| problem = getType(text, 'Проблема') |
| sector = sector.replace(' ', ' ') |
| product = product.replace(' ', ' ') |
| problem = problem.replace(' ', ' ') |
| |
| if apptype == 'Жалобы' and sector != '' and product != '' and problem != '' : |
| if sector not in sectors : |
| sectors.append(sector) |
| |
| if sector not in products : |
| products[sector] = [] |
| |
| if product not in products[sector] : |
| products[sector].append(product) |
| |
| if sector not in problems : |
| problems[sector] = {} |
| |
| if product not in problems[sector] : |
| problems[sector][product] = [] |
| |
| if problem not in problems[sector][product] : |
| problems[sector][product].append(problem) |
|
|
| def getCategory(text, categories) : |
| found = False |
| text = text.lower() |
| for category in categories : |
| if category.lower() in text : |
| found = True |
| break |
|
|
| if found == False : |
| category = '' |
|
|
| return category |
|
|
| def create_headers(): |
| api_key = os.getenv("DEEPINFRA_API_KEY", None) |
| if api_key is None: |
| print("DEEPINFRA_API_KEY is not set!") |
| headers = {"Content-Type": "application/json"} |
| headers["Authorization"] = os.getenv("DEEPINFRA_API_KEY", None) |
| return headers |
| |
| def create_messages(prompt, system_prompt = None): |
| messages = [] |
| |
| if system_prompt is not None: |
| messages.append({"role": "system", "content": system_prompt}) |
| messages.append({"role": "user", "content": prompt}) |
| return messages |
| |
| def create_request(prompt, system_prompt = None): |
| request = { |
| "stream": False, |
| "model": os.getenv("DEEPINFRA_MODEL", "meta-llama/Llama-3.3-70B-Instruct-Turbo"), |
| "temperature": 0.14, |
| "top_p": 0.95, |
| "min_p": 0.05, |
| "frequency_penalty":-0.001, |
| "presence_penalty": 1.3, |
| "seed": 42, |
| "max_tokens": 1000 |
| } |
|
|
| request["messages"] = create_messages(prompt, system_prompt) |
| return request |
|
|
| async def getResponse(prompt): |
| headers = { |
| 'Content-Type': 'application/json' |
| } |
| |
| async with httpx.AsyncClient() as client: |
| request = create_request(prompt, system_prompt=None) |
| response = await client.post(f"https://api.deepinfra.com/v1/openai/chat/completions", headers=create_headers(), json=request, timeout=httpx.Timeout(connect=5.0, read=60.0, write=180, pool=10)) |
| if response.status_code == 200: |
| return response.json()["choices"][0]["message"]["content"] |
| else: |
| logging.error(f"Request failed: status code {response.status_code}") |
| logging.error(response.text) |
|
|
| async def getCategoryFromLLM(prompt, categories) : |
| category = '' |
| for j in range(5) : |
| result = await getResponse(prompt) |
| category = getCategory(result, categories) |
| if category != '' : |
| break |
| |
| prompt += '.' |
| |
| return category, result |
|
|
| def getAccuracy(answers, trueanswers) : |
| count = 0 |
| for i in range(len(trueanswers)) : |
| if answers[i] == trueanswers[i] : |
| count += 1 |
|
|
| return count / len(trueanswers) |
|
|
| async def getAnswers(applications, prefix, categories, answers) : |
| |
| output = [] |
| for i in range(len(applications)) : |
| text = applications[i] |
| prompt = prefix + text |
| category, response = await getCategoryFromLLM(prompt, categories) |
| |
| answer = '' |
| for j in range(len(categories)) : |
| if category == categories[j] : |
| answer = answers[j] |
| break |
|
|
| brief = response.replace('\n', '') |
| if len(brief) > 80 : |
| brief = brief[:80] + '...' |
| |
| if verbose : |
| print(i, ':', answer, ' \tLLM output :', brief) |
| |
| output.append(answer) |
|
|
| return output |
|
|
| async def getSector(application) : |
|
|
| sectortext = '' |
| for j in range(len(sectors)) : |
| sectortext += str(j) + '. ' + sectors[j] + '. ' |
|
|
| prompt = '''Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. Ты сортируешь заявления клиентов. |
| Ты не отвечаешь на вопросы, не комментируешь, не выражаешь эмоций, не выражаешь соображений по теме заявления. |
| Ты извлекаешь информацию. Ты не анализируешь. |
| Ты выполняешь только эту задачу: ты определяешь категорию заявления. Для этого ты используешь ТОЛЬКО список возможных категорий, который я тебе предоставляю. |
| Ты выбираешь только ТУ категорию, которая на сто процентов соответсвует обращению. Проверь свой ответ дважды. |
| Ты всегда используешь такой формат ответа: "название категории". |
| Если в тексте обращения есть аббревиатуры "МФО", "МФК" или "МКК", ты должен выбрать категорию "Микрофинансовые организации". |
| Если в тексте обращения есть аббревиатуры "ОСАГО" или "КАСКО", ты должен выбрать категорию "Субъекты страхового дела". |
| Список категорий: |
| ''' + sectortext + '\nЗаявление: ' + application |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| sector, response = await getCategoryFromLLM(prompt, sectors) |
| |
| if verbose : |
| print(sector) |
|
|
| return sector |
|
|
| async def getProduct(application, sector) : |
| product = '' |
| |
| if sector != '' : |
| subproducts = products[sector] |
| |
| producttext = '' |
| for j in range(len(subproducts)) : |
| producttext += str(j) + '. ' + subproducts[j] + '. ' |
| |
| prompt = 'Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. Ты сортируешь заявления клиентов. Ты не отвечаешь на вопросы, не комментируешь, \ |
| не выражаешь эмоций, не выражаешь соображений по теме обращения. Ты извлекаешь персональные данные. Ты не анализируешь. \ |
| Ты выполняешь только эту задачу: \ |
| ты определяешь категорию заявления. Для этого ты используешь ТОЛЬКО список возможных категорий, который я тебе предоставляю. \ |
| Ты выбираешь только ТУ категорию, которая на сто процентов соответсвует заявлению. Проверь свой ответ дважды. \ |
| Ты всегда используешь такой формат ответа: "название категории". \n\ |
| Список категорий:\n' + producttext + '\nЗаявление: ' + application |
|
|
| product, response = await getCategoryFromLLM(prompt, subproducts) |
| |
| if verbose : |
| print(product) |
|
|
| return product |
|
|
| async def getProblem(application, sector, product) : |
| problem = '' |
| if sector != '' and product != '': |
| subpproblems = problems[sector][product] |
| |
| problemtext = '' |
| for j in range(len(subpproblems)) : |
| problemtext += str(j) + '. ' + subpproblems[j] + '. ' |
| |
| prompt = 'Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. Ты сортируешь заявления клиентов. Ты не отвечаешь на вопросы, не комментируешь, \ |
| не выражаешь эмоций, не выражаешь соображений по теме обращения. Ты извлекаешь персональные данные. Ты не анализируешь. \ |
| Ты выполняешь только эту задачу: \ |
| ты определяешь категорию заявления. Для этого ты используешь ТОЛЬКО список возможных категорий, который я тебе предоставляю. \ |
| Ты выбираешь только ТУ категорию, которая на сто процентов соответсвует заявлению. Проверь свой ответ дважды. \ |
| Ты всегда используешь такой формат ответа: "название категории". \n\ |
| Список категорий:\n' + problemtext + '\nЗаявление: ' + application |
| |
| problem, response = await getCategoryFromLLM(prompt, subpproblems) |
| |
| if verbose : |
| print(problem) |
|
|
| return problem |
|
|
| async def getAuthor(application) : |
| prefix = '''Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. Ты не отвечаешь на вопросы, не комментируешь, |
| не выражаешь эмоций, не выражаешь соображений по теме обращения. |
| Ты извлекаешь информацию из заявления. Ты отвечаешь на МОЙ вопрос: |
| "Кто является заявителем в заявлении?". Ты называешь имя заявителя в формате: "Заявитель: Фамилия Имя Отчество". |
| Если заявиитель не указан в заявлении, ты отвечаешь: "Заявитель: не указан". |
| Ты не комментируешь, не обясняешь, не выражаешь мысли, вообще ничего больше не говоришь. |
| Обращение: ''' |
|
|
| prompt = prefix + application |
| |
| response = await getResponse(prompt) |
| response = response.replace('.', '. ') |
| name = 'не указан' |
| if name not in response : |
| m = re.search(r'Заявитель: [А-Я][а-я][\w\.]+ [А-Я][\w\.]+ [А-Я][\w\.]+', response) |
| if m : |
| name = response[m.start() + 11 : m.end()] |
| else : |
| m = re.search(r'Заявитель: [А-Я][а-я][\w]+ [А-Я][а-я][\w]+', response) |
| if m : |
| name = response[m.start() + 11 : m.end()] |
|
|
| if verbose : |
| print(name, '\n', response[:100].replace('\n', ' ')) |
| |
| return name |
|
|
| async def checkContractNumber(application) : |
| categories = ['да', 'нет'] |
| answers = ['да', 'нет'] |
| |
| prefix = '''Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. Ты извлекаешь информацию из заявлений. |
| Ты не отвечаешь на вопросы, не комментируешь, не выражаешь эмоций, не выражаешь соображений по теме обращения. |
| Ты только отвечаешь на МОЙ вопрос: "Имеется ли в заявлении указанный номер договора?". |
| Ты отвечаешь либо ТАК "ответ: да, имеется" ЛИБО так "ответ: нет, не имеется". Конец ответа. |
| Если в заявлении нет слова "договор", ты отвечаешь "ответ: нет, не имеется" |
| Ты не комментируешь, не объясняешь, не выражаешь мысли, вообще ничего больше не говоришь. |
| Заявление: ''' |
|
|
| ifcontract = await getAnswers([application], prefix, categories, answers) |
|
|
| return ifcontract[0] |
|
|
| async def checkIfIdentified(application) : |
| сategories = ['нельзя', 'можно'] |
| answers = ['нет', 'да'] |
|
|
| prefix = '''Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. |
| Ты не отвечаешь на вопросы, не комментируешь, не выражаешь эмоций, не выражаешь соображений по теме жалобы. |
| Ты ищешь в заявлении объект жалобы: "Можно ли идентицифировать в заявлении объект жалобы (тот, на кого жалуется заявитель)?". |
| Твой ответ ВСЕГДА состоит из ТРЕХ слов: ты отвечаешь либо ТАК "да, можно", ЛИБО так "нет, нельзя". |
| Ты не комментируешь, не объясняешь, не выражаешь мысли, вообще ничего больше не говоришь. |
| Жалоба: ''' |
| |
| ifidentified = await getAnswers([application], prefix, сategories, answers) |
|
|
| return ifidentified[0] |
|
|
| async def checkIfPerson(application) : |
| categories = ['физическое лицо', 'юридическое лицо'] |
| answers = ['Физ.лицо', 'Юр.лицо'] |
| |
| prefix = '''Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. Ты сортируешь "заявления" клиентов. |
| Ты не отвечаешь на вопросы, не комментируешь, не выражаешь эмоций, не выражаешь соображений по теме обращения. |
| Ты извлекаешь информацию. Ты не анализируешь. |
| Ты отвечаешь ТОЛЬКО на мои вопросы. Ты определяешь кем является заявитель: "физическое лицо" или "юридическое лицо". |
| Условие: если заявление написано в первом лице (местоимения Я, МНЕ, МНОЮ, МОЕ, МЕНЯ), то это физическое лицо, НО если заявление написано в третьем лице, то это юридическое лицо. |
| Ты отвечаешь только так: "Заявитель: юрдическое лицо" или "Заявитель: физическое лицо". |
| Ты не комментируешь, не обясняешь, не выражаешь мысли, вообще ничего больше не говоришь. |
| Заявление: ''' |
| |
| ifperson = await getAnswers([application], prefix, categories, answers) |
|
|
| return ifperson[0] |
|
|
| async def checkIfcomission(application) : |
| categories = ['не касается', 'касается'] |
| answers = ['нет', 'да'] |
| |
| prefix = '''Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. Ты извлекаешь информацию из заявлений. |
| Ты не отвечаешь на вопросы, не комментируешь, не выражаешь эмоций, не выражаешь соображений по теме обращения. |
| Ты только отвечаешь на МОЙ вопрос: "Касается ли заявление комиссии за обслуживание рублевого счета?". |
| Ты отвечаешь либо ТАК "ответ: да, касается" ЛИБО так "ответ: нет, не касается". Конец ответа. |
| Если в заявлении нет слова "комиссия", ты отвечаешь "ответ: нет, не касается" |
| Ты не комментируешь, не объясняешь, не выражаешь мысли, вообще ничего больше не говоришь. |
| Заявление: ''' |
| |
| ifсomission = await getAnswers([application], prefix, categories, answers) |
|
|
| return ifсomission[0] |
|
|
| async def getContractData(application) : |
| prefix = '''Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. Ты не отвечаешь на вопросы, не комментируешь, |
| не выражаешь эмоций, не выражаешь соображений по теме обращения.Ты выполняешь только эту задачу: |
| ты извлекаешь из заявления только *номер ДОГОВОРА* и "дата ДОГОВОРА". |
| Ты всегда используешь только такой формат: "Номер договора: *номер ДОГОВОРА*, Дата: *дата этого договора*;". |
| Если указан любой другой номер, но НЕ номер ДОГОВОРА, то ты отвечаешь так: "Номер договора не указан." |
| Ты должен убедиться, что слово "договор" присутствует рядом с указанным номером и исключить другие документы, такие как счета или заказы, |
| например: "В соответствии с Договором № 0001 от 01.01.2022 года...". |
| В этом примере номером договора является "0001" и датой договора является "01.01.2022". |
| Даты договоров должны быть указаны в формате "дд.мм.гггг", где "дд" - это число от 01 до 31, "мм" - число от 01 до 12, |
| а "гггг" - четырехзначное число года. Между днями, месяцами и годами должны быть разделители, например, точки или тире. |
| Ты больше НИЧЕГО не говоришь, не комментируешь, не объясняешь, не добавляешь. |
| Заявление: ''' |
|
|
| prompt = prefix + application |
| response = await getResponse(prompt) |
| response = response.replace(';', '\n') |
| response = response.replace('\\\\', '') |
| l = response.split('\n') |
| ll = [] |
| for s in l : |
| s = s.strip() |
| if 'Номер договора:' == s[:15] : |
| ll.append(s) |
|
|
| result = '\n'.join(ll) |
|
|
| if result == '' : |
| result = 'не указаны' |
|
|
| if verbose : |
| print(result) |
| |
| |
| return result |
| |
| async def getPersons(application) : |
| |
| |
| |
| |
| |
| |
| |
|
|
| prefix = '''Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. Ты излекаешь информацию из заявления. Ты не отвечаешь на вопросы, не комментируешь, |
| не выражаешь эмоций, не выражаешь соображений по теме заявления. Ты извлекаешь персональные данные. Ты не анализируешь. |
| Ты выполняешь только эту задачу: |
| ты читаешь заявление и извлекаешь из заявления все встретившиеся Фамилии Имена Отчества. |
| Ты отвечаешь в формате: "ФИО: Фамилия Имя Отчество ;" или "ФИО: Фамилия И. О. ;". |
| Если в заявлении не указаны имена людей, ты отвечаешь "ФИО не указаны". |
| Перед ответом убедись, что "ФИО" - это человеческие фамилия, имя, отчество. |
| Ты больше ничего не говоришь, не комментируешь, не объясняешь, не добавляешь. |
| Заявление: ''' |
|
|
| prompt = prefix + application |
| |
| response = await getResponse(prompt) |
| response = response.replace('указаны.', 'указаны') |
| response = response.replace('.', '. ') |
| response = response.replace(';', '\n') |
| |
| l = response.split('\n') |
| ll = [] |
| for s in l : |
| s = s.strip() |
| if 'ФИО: ' == s[:5] and 'ФИО: не указаны' not in s: |
| ss = '' |
| s = s[5:] |
| s = re.sub('\(.+\)', '', s) |
| s = s.replace('ч.', 'ч') |
| s = s.replace('а.', 'а') |
| s = s.replace('Президент Российской Федерации', '') |
| s = s.replace(',', '').strip() |
| |
| |
| |
| |
| m = re.search(r'[А-Я][а-я][\w\.]+ [А-Я][\w\.]+ [А-Я][\w\.]+', s) |
| if m : |
| ss = s[m.start() : m.end()] |
| else : |
| m = re.search(r'[А-Я][а-я][\w]+ [А-Я][а-я][\w]+', s) |
| if m : |
| ss = s[m.start(): m.end()] |
|
|
| if ss != '' : |
| ll.append(ss) |
|
|
| result = '\n'.join(ll) |
|
|
| if result == '' : |
| result = 'не указаны' |
| |
| names = result |
|
|
| if verbose : |
| print(names, '\n', response[:100].replace('\n', ' ')) |
| |
| return names |
|
|
| def ifLatin(s) : |
| ss = s.lower() |
| result = False |
| for c in ss : |
| if c in 'abcdefghijklmnopqrstuvwxyz' : |
| result = True |
| break |
|
|
| return result |
|
|
| stoplist = ['микрофинансовые организации', |
| 'полиция', |
| 'Мурманский край', |
| 'Перми', |
| 'Краснодар', |
| 'центр занятости населения Владимирской области', |
| 'банкомат N 7032 банка РСБ', |
| 'банк', |
| 'Криптобиржа', |
| 'Nasdaq', |
| 'Государство', |
| 'Департаменты Москвы', |
| '"Волгабанк" и Никулин', |
| 'Тендеры', |
| 'Уголовные дела', |
| 'Управляющими финансовой пирамидой "Волгабанк"', |
| 'Санации банка', |
| 'Криптобиржи', |
| 'Уголовная ответственность', |
| 'Статьей 185.3 УК РФ', |
| 'С ТОЙОТА КРАУН Х568ПУ69', |
| 'Республика Беларусь', |
| 'Минфин Республики Беларусь', |
| 'Московская биржа (АО НРД)', |
| 'АО', |
| 'прокуратура РФ', |
| 'приемная президента РФ', |
| 'страховая компания.', |
| 'микрофинансовые организации', |
| 'Следственный комитет', |
| 'прокуратура', |
| 'юристы', |
| 'ИНН 7854523125', |
| 'кредитная организация', |
| 'прокуратура РФ', |
| 'приемная президента РФ.', |
| 'фин услуги', |
| 'суд', |
| 'банк', |
| 'микрофинансовые организации', |
| 'Государственный рееestr МФО', |
| 'полиция', |
| 'Прокуратура РФ', |
| 'Приемная президента РФ', |
| 'фирма', |
| 'скоринг бюро', |
| 'правоохранительные органы', |
| 'Департамент здравоохранения г', |
| 'Страховщик', |
| 'Статьей 185'] |
|
|
| async def getCompanies(application) : |
| prefix = '''Ты мой помощник. Ты отвечаешь только на РУССКОМ языке. Ты извлекаешь информацию из заявления. |
| Ты не отвечаешь на вопросы, не комментируешь, не выражаешь эмоций, не выражаешь соображений по теме заявления. |
| Ты выполняешь только эту задачу: ты извлекаешь из заявления только *названия юридических ОРГАНИЗАЦИЙ*. |
| Ты всегда используешь только этот формат: "Организация: *название организации*;". |
| Ты больше НИЧЕГО не говоришь, не комментируешь, не объясняешь, не добавляешь. |
| Если названия организаций отсутствуют, то ты даешь только ТАКОЙ ответ: "не указано". |
| Твой ответ состоит только из одного слова - *название организации.* Тебе запрещено общаться, ты всегда следуешь формату. |
| Польуйся моими советами, как определить, что это действительно название организации: |
| Памятка: Юридическая форма: Название может содержать слова, указывающие на юридическую форму организации, такие как "корпорация", |
| "общество с ограниченной ответственностью", "партнерство" и т.д. Название может состоять из аббревиатуры, |
| которая представляет собой сокращение от полного названия организации. Название может содержать описательные слова или фразы, |
| которые указывают на вид деятельности организации, ее цели или ценности. |
| Соответствие формальным требованиям: Названия организаций, связанных с денежно-кредитной политикой, платёжной системой и финансовым регулированием, |
| обычно соответствуют определенным формальным требованиям, таким как использование определенных слов, например "банк", "компания", "организация" и т.д. |
| Заявление: ''' |
|
|
| prompt = prefix + application |
| response = await getResponse(prompt) |
| l = response.split('Организация: ') |
| ll = [] |
| for i in range(len(l)) : |
| Inf = 1000000 |
| s = l[i] |
| x = s.find(';') |
| y = s.find('.') |
| z = s.find('(') |
| if x == -1 : |
| x = Inf |
| |
| if y == -1 : |
| y = Inf |
| |
| if z == -1 : |
| z = Inf |
| |
| x = min(x, y, z) |
| |
| if x != -1 : |
| s = l[i][:x] |
|
|
| s = s.strip() |
| if s != '' and not ifLatin(s) and s not in ll and s not in stoplist: |
| ll.append(s) |
|
|
| result = '\n'.join(ll) |
|
|
| if result == '' : |
| result = 'не указаны' |
|
|
| if verbose : |
| print(result) |
| |
| return result |
| |
| async def getApplicationInfo(application) : |
| author = await getAuthor(application) |
| persons = await getPersons(application) |
| companies = await getCompanies(application) |
| contractdata = await getContractData(application) |
| sector = await getSector(application) |
| product = await getProduct(application, sector) |
| problem = await getProblem(application, sector, product) |
| |
| ifcontract = await checkContractNumber(application) |
| ifidentified = await checkIfIdentified(application) |
| ifperson = await checkIfPerson(application) |
| ifcomission = await checkIfcomission(application) |
| |
| app_info = {} |
| app_info['Заявитель'] = author |
| app_info['Физлица'] = persons |
| app_info['Организации'] = companies |
| app_info['Данные договора'] = contractdata |
| app_info['Заявитель физическое или юридическое лицо?'] = ifperson |
| app_info['Можно ли идентифицировать лицо, на которого пожаловались?'] = ifidentified |
| app_info['Указан ли в обращении номер договора?'] = ifcontract |
| app_info['Жалоба касается комиссии за обслуживание рублевого счета?'] = ifcomission |
| app_info['Сектор'] = sector |
| app_info['Продукт'] = product |
| app_info['Проблема'] = problem |
| |
| if verbose : |
| print() |
| print('Заявитель', author) |
| print('Физлица', persons) |
| print('Организации', companies) |
| print('Данные договора', contractdata) |
| print('Заявитель физическое или юридическое лицо?', ifperson) |
| print('Можно ли идентифицировать лицо, на которого пожаловались?', ifidentified) |
| print('Указан ли в обращении номер договора?', ifcontract) |
| print('Жалоба касается комиссии за обслуживание рублевого счета?', ifcomission) |
| print('Сектор', sector) |
| print('Продукт', product) |
| print('Проблема', problem) |
| |
| return app_info |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |