@@ -1,11 +1,21 @@ +import base64 import time +from io import BytesIO + +import filetype + from _decimal import Decimal from datetime import timedelta -from typing import Any, Iterator + +from django.db.models.fields.files import FieldFile from messages.models import Message from ml_model.services.base import SimpleService -from ml_model.tasks import replicate_run +from ml_model.tasks import openrouter_run +from tools.chats.models import Chat +from tools.copywrite.models import Copywrite +from tools.public_api.models import APIStore +from PIL import Image class Llama(SimpleService): @@ -14,16 +24,26 @@ class Llama(SimpleService): contains abstract method make, which makes a generation """ - price = Decimal('1.078') + TOKENS_COST = { + 'llama-3.3-70b-instruct': {'input': Decimal('84'), 'output': Decimal('84')}, # 1M tokens + 'llama-4-maverick': {'input': Decimal('180'), 'output': Decimal('180'), 'input_imgs': Decimal('200.52')}, # 1M tokens + } - def calculate_price(self, process_time: timedelta) -> Decimal: - price = Decimal(process_time.total_seconds()) * self.price + def calculate_price( + self, version: str, input_tokens: int, output_tokens: int, image: FieldFile + ) -> Decimal: + price_map = self.TOKENS_COST[version.split('/')[1]] + price = ( + input_tokens * price_map['input'] / 1_000_000 + output_tokens * price_map['output'] / 1_000_000 + ) + if image: + price += price_map['input_imgs'] / 1_000 return price.quantize(Decimal('0.1'), rounding='ROUND_UP') - def save_results(self, r: Iterator[Any], t: timedelta, save: bool = True) -> list[Message]: + def save_results(self, content: str, t: timedelta, save: bool = True) -> list[Message]: msgs = [ Message( - content=''.join(word for word in r), + content=content, content_object=self.store, elapsed_time=t, ) @@ -33,29 +53,67 @@ class Llama(SimpleService): return msgs def make(self, input_message: Message, save: bool = True) -> list[Message]: - info = input_message.info - context_ids: list[str] = info.pop('context_messages', []) - content = ( - ''.join( - msg.content + ' ' - for msg in Message.objects.filter(uid__in=context_ids, chats_chats_messages=self.store) - ) - + input_message.content - ) - version = info.pop('version', 'llama2-70b') - if version == 'llama2-70b': - self.model_version = ( - 'meta/llama-2-70b-chat:35042c9a33ac8fd5e29e27fb3197f33aa483f72c2ce3b0b9d201155c7fd2a287' - ) - callback_data = dict( - { - 'prompt': content, - **info, - } - ) start_time = time.time() - result = replicate_run(self.model_version, callback_data) + version = f'meta-llama/{input_message.info.pop('version', 'llama-4-maverick')}' + callback_data = {'provider': {'order': ['DeepInfra']}, **input_message.info} + messages = self.get_chat_history() + messages.append({'role': 'user', 'content': input_message.content}) + image = input_message.file + if image: + kind = filetype.guess(image.read(20)) + mime = kind.mime if kind else 'application/octet-stream' + normalized_image = Image.open(image) + format = 'jpeg' if kind.extension == 'jpg' else kind.extension + buf = BytesIO() + normalized_image.save(buf, format=format) + image_url = f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' + buf.close() + messages[-1]['content'] = [ + {'type': 'text', 'text': input_message.content}, + {'type': 'image_url', 'image_url': {'url': image_url}}, + ] + result = openrouter_run(version, messages, callback_data, 'Llama') process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, process_time=process_time) - msgs = self.save_results(result, process_time, save) + self.handle_invoice( + input_message.content_object.model, + version=version, + input_tokens=result[1], + output_tokens=result[2], + image=image, + ) + msgs = self.save_results(result[0], process_time) return msgs + + def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500) -> list[dict[str, str | list]]: + if isinstance(self.store, Chat): + air_messages = list( + reversed( + Message.objects.filter( + chats_chats_messages=self.store, is_deleted=False, is_sent=True + ).order_by('-created_at')[1:message_limit + 1] + ) + ) + elif isinstance(self.store, APIStore): + air_messages = [] + elif isinstance(self.store, Copywrite): + air_messages = list( + reversed( + Message.objects.filter( + copywrite_copywrites_messages=self.store, + is_deleted=False, + is_sent=True, + ).order_by('-created_at')[:message_limit] + ) + ) + memory = [] + for msg in air_messages: + content = msg.content or '' + if msg.from_model: + memory.append({'role': 'assistant', 'content': content}) + else: + memory.append({'role': 'user', 'content': content}) + character_length = sum(len(content['content']) for content in memory) + while character_length > max_character_limit: + character_length -= len(memory.pop(0)['content']) + + return memory