From 6555bf7b4d5bf50209be50404e343a3f16da0a4c Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Wed, 26 Feb 2025 16:35:42 +0000 Subject: [PATCH 01/11] max tokens --- labs/core/models.py | 3 ++- labs/embeddings/gemini.py | 5 +++-- labs/embeddings/ollama.py | 5 +++-- labs/embeddings/openai.py | 5 +++-- labs/fixtures/model.json | 4 +++- labs/llm/anthropic.py | 11 ++++++++--- labs/llm/gemini.py | 5 +++-- labs/llm/ollama.py | 11 +++++++---- labs/llm/openai.py | 8 +++++--- 9 files changed, 37 insertions(+), 20 deletions(-) diff --git a/labs/core/models.py b/labs/core/models.py index 462f084..70f19dd 100644 --- a/labs/core/models.py +++ b/labs/core/models.py @@ -100,6 +100,7 @@ class Model(models.Model): provider = models.CharField(choices=ProviderEnum.choices()) model_name = models.CharField(max_length=255) active = models.BooleanField(default=True) + max_output_tokens = models.IntegerField(default=2048) @staticmethod def get_active_embedding_model() -> Tuple[Embedder, str]: @@ -119,7 +120,7 @@ def _get_active_provider_model(model_type: Literal["embedding", "llm", "vectoriz # Load associated provider variables Variable.load_provider_keys(model.provider) - return provider_model_class[model.provider][model_type], model.model_name + return provider_model_class[model.provider][model_type], model def __str__(self): return f"{self.model_type} {self.provider} {self.model_name}" diff --git a/labs/embeddings/gemini.py b/labs/embeddings/gemini.py index cff5364..6b6e390 100644 --- a/labs/embeddings/gemini.py +++ b/labs/embeddings/gemini.py @@ -1,10 +1,11 @@ import os import google.generativeai as genai from embeddings.embedder import Embeddings +from core.models import Model class GeminiEmbedder: - def __init__(self, model: str): - self._model_name = model + def __init__(self, model: Model): + self._model_name = model.model_name api_key = os.environ.get("GEMINI_API_KEY") genai.configure(api_key=api_key) diff --git a/labs/embeddings/ollama.py b/labs/embeddings/ollama.py index e65f2de..7f8b30e 100644 --- a/labs/embeddings/ollama.py +++ b/labs/embeddings/ollama.py @@ -1,11 +1,12 @@ from django.conf import settings from embeddings.embedder import Embeddings from ollama import Client +from core.models import Model class OllamaEmbedder: - def __init__(self, model): - self._model_name = model + def __init__(self, model: Model): + self._model_name = model.model_name self._client = Client(settings.LOCAL_LLM_HOST) def embed(self, prompt, *args, **kwargs) -> Embeddings: diff --git a/labs/embeddings/openai.py b/labs/embeddings/openai.py index 28c9e7a..3fbca41 100644 --- a/labs/embeddings/openai.py +++ b/labs/embeddings/openai.py @@ -2,11 +2,12 @@ import openai from embeddings.embedder import Embeddings from litellm import embedding +from core.models import Model class OpenAIEmbedder: - def __init__(self, model): - self._model_name = model + def __init__(self, model: Model): + self._model_name = model.model_name openai.api_key = os.environ.get("OPENAI_API_KEY") def embed(self, prompt, *args, **kwargs) -> Embeddings: diff --git a/labs/fixtures/model.json b/labs/fixtures/model.json index 7edb11e..9a82e3c 100644 --- a/labs/fixtures/model.json +++ b/labs/fixtures/model.json @@ -36,6 +36,7 @@ "model_type": "LLM", "provider": "OLLAMA", "model_name": "llama3.2:latest", + "max_output_tokens": 2048, "active": false } }, @@ -66,7 +67,8 @@ "model_type": "LLM", "provider": "ANTHROPIC", "model_name": "claude-3-5-sonnet-20241022", + "max_output_tokens": 8192, "active": false } } -] +] \ No newline at end of file diff --git a/labs/llm/anthropic.py b/labs/llm/anthropic.py index 2741690..d4830fa 100644 --- a/labs/llm/anthropic.py +++ b/labs/llm/anthropic.py @@ -1,6 +1,7 @@ import logging import os from typing import Any, Dict, List, Tuple, cast +from core.models import Model from anthropic import Anthropic from anthropic.types import Message, TextBlock @@ -9,8 +10,9 @@ class AnthropicRequester: - def __init__(self, model: str): - self._model_name = model + def __init__(self, model: Model): + self._model_name = model.model_name + self._model_max_output_tokens = model.max_output_tokens api_key = os.environ.get("ANTHROPIC_API_KEY") self.client = Anthropic(api_key=api_key) @@ -25,7 +27,10 @@ def completion_without_proxy( try: response = self.client.messages.create( - model=self._model_name, system=system_prompt, messages=user_messages, max_tokens=8192 + model=self._model_name, + system=system_prompt, + messages=user_messages, + max_tokens=self._model_max_output_tokens, ) response_steps = self.response_to_steps(response) diff --git a/labs/llm/gemini.py b/labs/llm/gemini.py index c00fc34..6cf4fdd 100644 --- a/labs/llm/gemini.py +++ b/labs/llm/gemini.py @@ -1,13 +1,14 @@ import os import json from typing import List, Dict, Tuple, Any +from core.models import Model import google.generativeai as genai class GeminiRequester: - def __init__(self, model: str): - self._model_name = model + def __init__(self, model: Model): + self._model_name = model.model_name api_key = os.environ.get("GEMINI_API_KEY") genai.configure(api_key=api_key) self.generative_model = genai.GenerativeModel(self._model_name) diff --git a/labs/llm/ollama.py b/labs/llm/ollama.py index 4a2fb57..f7cca29 100644 --- a/labs/llm/ollama.py +++ b/labs/llm/ollama.py @@ -1,18 +1,21 @@ from django.conf import settings +from core.models import Model +from typing import Tuple, Dict, Any from ollama import Client class OllamaRequester: - def __init__(self, model): - self._model_name = model + def __init__(self, model: Model): + self._model_name = model.model_name + self._model_max_output_tokens = model.max_output_tokens self._client = Client(settings.LOCAL_LLM_HOST) - def completion_without_proxy(self, messages, *args, **kwargs): + def completion_without_proxy(self, messages, *args, **kwargs) -> Tuple[str, Dict[str, Any]]: response = self._client.chat( model=self._model_name, messages=messages, format="json", - options={"num_ctx": 8192}, + options={"num_ctx": self._model_max_output_tokens}, *args, **kwargs, ) diff --git a/labs/llm/openai.py b/labs/llm/openai.py index 6543110..f478cdf 100644 --- a/labs/llm/openai.py +++ b/labs/llm/openai.py @@ -1,15 +1,17 @@ import os +from core.models import Model +from typing import Tuple, Dict, Any import openai from litellm import completion class OpenAIRequester: - def __init__(self, model): - self._model_name = model + def __init__(self, model: Model): + self._model_name = model.model_name openai.api_key = os.environ.get("OPENAI_API_KEY") - def completion_without_proxy(self, messages, *args, **kwargs): + def completion_without_proxy(self, messages, *args, **kwargs) -> Tuple[str, Dict[str, Any]]: return self._model_name, completion( model=self._model_name, messages=messages, From 1217e576adc3e4dfea2de01144fb1de4272e6e48 Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Wed, 26 Feb 2025 17:46:28 +0000 Subject: [PATCH 02/11] separate models --- labs/core/admin.py | 40 +++++-- labs/core/factories.py | 21 +++- labs/core/forms.py | 53 +++++---- ..._model_unique_active_embedding_and_more.py | 4 +- ...ingmodel_llmmodel_delete_model_and_more.py | 112 ++++++++++++++++++ labs/core/models.py | 85 +++++++------ labs/embeddings/gemini.py | 5 +- labs/embeddings/ollama.py | 5 +- labs/embeddings/openai.py | 5 +- labs/fixtures/embedding.json | 29 +++++ labs/fixtures/llm.json | 40 +++++++ labs/fixtures/model.json | 74 ------------ labs/llm/anthropic.py | 5 +- labs/llm/gemini.py | 5 +- labs/llm/ollama.py | 5 +- labs/llm/openai.py | 5 +- labs/tasks/llm.py | 8 +- labs/tasks/logging.py | 6 +- labs/tests/conftest.py | 37 +++--- labs/tests/test_llm.py | 32 ++--- 20 files changed, 357 insertions(+), 219 deletions(-) create mode 100644 labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py create mode 100644 labs/fixtures/embedding.json create mode 100644 labs/fixtures/llm.json delete mode 100644 labs/fixtures/model.json diff --git a/labs/core/admin.py b/labs/core/admin.py index 82169fe..0a76dca 100644 --- a/labs/core/admin.py +++ b/labs/core/admin.py @@ -1,17 +1,39 @@ from django.contrib import admin from django.urls import reverse -from .forms import ProjectForm, ModelFormSet +from .forms import ProjectForm, EmbeddingModelFormSet, LLMModelFormSet from .mixins import JSONFormatterMixin -from .models import Model, Project, Prompt, Variable, VectorizerModel, WorkflowResult +from .models import ( + EmbeddingModel, LLMModel, Project, Prompt, + Variable, VectorizerModel, WorkflowResult +) + +@admin.register(EmbeddingModel) +class EmbeddingModelAdmin(admin.ModelAdmin): + list_display = ("id", "provider", "name", "active") + list_display_links = ("id",) + list_editable = ("provider", "name", "active") + list_filter = ("provider", "name") + search_fields = ("provider", "name") + + def has_add_permission(self, request): + return False + + def has_delete_permission(self, request, obj=None): + return False -@admin.register(Model) -class ModelAdmin(admin.ModelAdmin): - list_display = ("id", "model_type", "provider", "model_name", "active") + def get_changelist_formset(self, request, **kwargs): + kwargs["formset"] = EmbeddingModelFormSet + return super().get_changelist_formset(request, **kwargs) + + +@admin.register(LLMModel) +class LLMModelAdmin(admin.ModelAdmin): + list_display = ("id", "provider", "name", "max_output_tokens", "active") list_display_links = ("id",) - list_editable = ("model_type", "provider", "model_name", "active") - list_filter = ("provider", "model_name") - search_fields = ("provider", "model_name") + list_editable = ("provider", "name", "max_output_tokens", "active") + list_filter = ("provider", "name") + search_fields = ("provider", "name", "max_output_tokens") def has_add_permission(self, request): return False @@ -20,7 +42,7 @@ def has_delete_permission(self, request, obj=None): return False def get_changelist_formset(self, request, **kwargs): - kwargs["formset"] = ModelFormSet + kwargs["formset"] = LLMModelFormSet return super().get_changelist_formset(request, **kwargs) diff --git a/labs/core/factories.py b/labs/core/factories.py index cea9dd8..ebc0445 100644 --- a/labs/core/factories.py +++ b/labs/core/factories.py @@ -2,8 +2,8 @@ from factory.django import DjangoModelFactory from .models import ( - Model, - ModelTypeEnum, + LLMModel, + EmbeddingModel, Project, Prompt, ProviderEnum, @@ -30,13 +30,22 @@ def default_vectorizer_value_validation(self, create, extracted, **kwargs): raise ValueError("Invalid vectorizer value") -class ModelFactory(DjangoModelFactory): +class LLMModelFactory(DjangoModelFactory): class Meta: - model = Model + model = LLMModel - model_type = factory.Iterator([mt.name for mt in ModelTypeEnum]) provider = factory.Iterator([provider.name for provider in ProviderEnum if provider != ProviderEnum.NO_PROVIDER]) - model_name = factory.Faker("word") + name = factory.Faker("word") + active = factory.Faker("boolean") + max_output_tokens = factory.Faker("integer") + + +class EmbeddingModelFactory(DjangoModelFactory): + class Meta: + model = EmbeddingModel + + provider = factory.Iterator([provider.name for provider in ProviderEnum if provider != ProviderEnum.NO_PROVIDER]) + name = factory.Faker("word") active = factory.Faker("boolean") diff --git a/labs/core/forms.py b/labs/core/forms.py index 82ba5a4..b077f28 100644 --- a/labs/core/forms.py +++ b/labs/core/forms.py @@ -1,6 +1,6 @@ import os -from core.models import Project, ModelTypeEnum +from core.models import Project from django.forms import ( ModelForm, BaseModelFormSet, ValidationError, ChoiceField ) @@ -30,30 +30,33 @@ class Meta: fields = ["name", "description", "path", "url"] -class ModelFormSet(BaseModelFormSet): +class EmbeddingModelFormSet(BaseModelFormSet): def clean(self): super().clean() - embedding_name = ModelTypeEnum.EMBEDDING.name - llm_name = ModelTypeEnum.LLM.name - counts = { - embedding_name: 0, - llm_name: 0, - } - errors = [] - - for form in self.forms: - model_type = form.cleaned_data["model_type"] - if model_type in counts and form.cleaned_data["active"]: - counts[model_type] += 1 - - for type_name, count in counts.items(): - if count != 1: - errors.append( - ValidationError( - f"You must have exactly 1 active '{type_name}', found {count}." - ) - ) - - if errors: - raise ValidationError(errors) + active_count = sum( + 1 + for form in self.forms + if form.cleaned_data and form.cleaned_data.get("active", False) + ) + + if active_count != 1: + raise ValidationError( + f"You must have exactly 1 active Embedding Model, found {active_count}." + ) + + +class LLMModelFormSet(BaseModelFormSet): + def clean(self): + super().clean() + + active_count = sum( + 1 + for form in self.forms + if form.cleaned_data and form.cleaned_data.get("active", False) + ) + + if active_count != 1: + raise ValidationError( + f"You must have exactly 1 active LLM Model, found {active_count}." + ) diff --git a/labs/core/migrations/0006_model_unique_active_embedding_and_more.py b/labs/core/migrations/0006_model_unique_active_embedding_and_more.py index 46b8063..c1f57f1 100644 --- a/labs/core/migrations/0006_model_unique_active_embedding_and_more.py +++ b/labs/core/migrations/0006_model_unique_active_embedding_and_more.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): constraint=models.UniqueConstraint( condition=models.Q( ("active", True), - ("model_type", core.models.ModelTypeEnum["EMBEDDING"]), + ("model_type", "Embedding"), ), fields=("model_type",), name="unique_active_embedding", @@ -24,7 +24,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="model", constraint=models.UniqueConstraint( - condition=models.Q(("active", True), ("model_type", core.models.ModelTypeEnum["LLM"])), + condition=models.Q(("active", True), ("model_type", "LLM")), fields=("model_type",), name="unique_active_llm", ), diff --git a/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py b/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py new file mode 100644 index 0000000..022d0d4 --- /dev/null +++ b/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py @@ -0,0 +1,112 @@ +# Generated by Django 5.1.6 on 2025-02-26 17:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0007_alter_model_provider_alter_variable_provider"), + ] + + operations = [ + migrations.CreateModel( + name="EmbeddingModel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "provider", + models.CharField( + choices=[ + ("NO_PROVIDER", "No provider"), + ("OPENAI", "OpenAI"), + ("OLLAMA", "Ollama"), + ("GEMINI", "Gemini"), + ("ANTHROPIC", "Anthropic"), + ] + ), + ), + ("name", models.CharField(max_length=255)), + ("active", models.BooleanField(default=True)), + ], + options={ + "verbose_name": "Embedding", + "verbose_name_plural": "Embeddings", + }, + ), + migrations.CreateModel( + name="LLMModel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "provider", + models.CharField( + choices=[ + ("NO_PROVIDER", "No provider"), + ("OPENAI", "OpenAI"), + ("OLLAMA", "Ollama"), + ("GEMINI", "Gemini"), + ("ANTHROPIC", "Anthropic"), + ] + ), + ), + ("name", models.CharField(max_length=255)), + ("active", models.BooleanField(default=True)), + ( + "max_output_tokens", + models.IntegerField(blank=True, default=None, null=True), + ), + ], + options={ + "verbose_name": "LLM", + "verbose_name_plural": "LLMs", + }, + ), + migrations.DeleteModel( + name="Model", + ), + migrations.AddIndex( + model_name="embeddingmodel", + index=models.Index( + fields=["provider", "name"], name="core_embedd_provide_04143f_idx" + ), + ), + migrations.AddConstraint( + model_name="embeddingmodel", + constraint=models.UniqueConstraint( + condition=models.Q(("active", True)), + fields=("provider",), + name="unique_active_embedding", + ), + ), + migrations.AddIndex( + model_name="llmmodel", + index=models.Index( + fields=["provider", "name"], name="core_llmmod_provide_8330c3_idx" + ), + ), + migrations.AddConstraint( + model_name="llmmodel", + constraint=models.UniqueConstraint( + condition=models.Q(("active", True)), + fields=("provider",), + name="unique_active_llm", + ), + ), + ] diff --git a/labs/core/models.py b/labs/core/models.py index 70f19dd..523e19e 100644 --- a/labs/core/models.py +++ b/labs/core/models.py @@ -27,16 +27,6 @@ vectorizer_model_class = {"CHUNK_VECTORIZER": ChunkVectorizer, "PYTHON_VECTORIZER": PythonVectorizer} - -class ModelTypeEnum(Enum): - EMBEDDING = "Embedding" - LLM = "LLM" - - @classmethod - def choices(cls): - return [(prop.name, prop.value) for prop in cls] - - class ProviderEnum(Enum): NO_PROVIDER = "No provider" OPENAI = "OpenAI" @@ -95,48 +85,69 @@ def __str__(self): return self.name -class Model(models.Model): - model_type = models.CharField(choices=ModelTypeEnum.choices()) +class EmbeddingModel(models.Model): provider = models.CharField(choices=ProviderEnum.choices()) - model_name = models.CharField(max_length=255) + name = models.CharField(max_length=255) active = models.BooleanField(default=True) - max_output_tokens = models.IntegerField(default=2048) - @staticmethod - def get_active_embedding_model() -> Tuple[Embedder, str]: - return Model._get_active_provider_model("embedding") + @classmethod + def get_active_model(cls) -> Tuple[Embedder, "EmbeddingModel"]: + try: + model = cls.objects.get(active=True) + except cls.DoesNotExist: + raise ValueError("No active embedding model configured") - @staticmethod - def get_active_llm_model() -> Tuple[Requester, str]: - return Model._get_active_provider_model("llm") + Variable.load_provider_keys(model.provider) + embedder_class = provider_model_class[model.provider]["embedding"] + return embedder_class, model - @staticmethod - def _get_active_provider_model(model_type: Literal["embedding", "llm", "vectorizer"]): - queryset = Model.objects.filter(model_type=model_type.upper(), active=True) - if not queryset.exists(): - raise ValueError(f"No {model_type} model configured") + def __str__(self): + return f"EmbeddingModel {self.provider} {self.name}" + + class Meta: + verbose_name = "Embedding" + verbose_name_plural = "Embeddings" + constraints = [ + models.UniqueConstraint( + fields=["provider"], + condition=Q(active=True), + name="unique_active_embedding", + ) + ] + indexes = [models.Index(fields=["provider", "name"])] - model = queryset.first() - # Load associated provider variables +class LLMModel(models.Model): + provider = models.CharField(choices=ProviderEnum.choices()) + name = models.CharField(max_length=255) + active = models.BooleanField(default=True) + max_output_tokens = models.IntegerField(null=True, blank=True, default=None) + + @classmethod + def get_active_model(cls) -> Tuple[Requester, "LLMModel"]: + try: + model = cls.objects.get(active=True) + except cls.DoesNotExist: + raise ValueError("No active llm model configured") + Variable.load_provider_keys(model.provider) - return provider_model_class[model.provider][model_type], model + llm_class = provider_model_class[model.provider]["llm"] + return llm_class, model def __str__(self): - return f"{self.model_type} {self.provider} {self.model_name}" + return f"LLMModel {self.provider} {self.name}" class Meta: + verbose_name = "LLM" + verbose_name_plural = "LLMs" constraints = [ models.UniqueConstraint( - fields=["model_type"], - condition=Q(model_type=ModelTypeEnum.EMBEDDING, active=True), - name="unique_active_embedding", - ), - models.UniqueConstraint( - fields=["model_type"], condition=Q(model_type=ModelTypeEnum.LLM, active=True), name="unique_active_llm" - ), + fields=["provider"], + condition=Q(active=True), + name="unique_active_llm", + ) ] - indexes = [models.Index(fields=["provider", "model_name"])] + indexes = [models.Index(fields=["provider", "name"])] class Project(models.Model): diff --git a/labs/embeddings/gemini.py b/labs/embeddings/gemini.py index 6b6e390..953e925 100644 --- a/labs/embeddings/gemini.py +++ b/labs/embeddings/gemini.py @@ -1,11 +1,10 @@ import os import google.generativeai as genai from embeddings.embedder import Embeddings -from core.models import Model class GeminiEmbedder: - def __init__(self, model: Model): - self._model_name = model.model_name + def __init__(self, model): + self._model_name = model.name api_key = os.environ.get("GEMINI_API_KEY") genai.configure(api_key=api_key) diff --git a/labs/embeddings/ollama.py b/labs/embeddings/ollama.py index 7f8b30e..1c693f6 100644 --- a/labs/embeddings/ollama.py +++ b/labs/embeddings/ollama.py @@ -1,12 +1,11 @@ from django.conf import settings from embeddings.embedder import Embeddings from ollama import Client -from core.models import Model class OllamaEmbedder: - def __init__(self, model: Model): - self._model_name = model.model_name + def __init__(self, model): + self._model_name = model.name self._client = Client(settings.LOCAL_LLM_HOST) def embed(self, prompt, *args, **kwargs) -> Embeddings: diff --git a/labs/embeddings/openai.py b/labs/embeddings/openai.py index 3fbca41..981eac4 100644 --- a/labs/embeddings/openai.py +++ b/labs/embeddings/openai.py @@ -2,12 +2,11 @@ import openai from embeddings.embedder import Embeddings from litellm import embedding -from core.models import Model class OpenAIEmbedder: - def __init__(self, model: Model): - self._model_name = model.model_name + def __init__(self, model): + self._model_name = model.name openai.api_key = os.environ.get("OPENAI_API_KEY") def embed(self, prompt, *args, **kwargs) -> Embeddings: diff --git a/labs/fixtures/embedding.json b/labs/fixtures/embedding.json new file mode 100644 index 0000000..413a21f --- /dev/null +++ b/labs/fixtures/embedding.json @@ -0,0 +1,29 @@ +[ + { + "model": "core.embeddingmodel", + "pk": 1, + "fields": { + "provider": "OLLAMA", + "name": "nomic-embed-text:latest", + "active": false + } + }, + { + "model": "core.embeddingmodel", + "pk": 2, + "fields": { + "provider": "OPENAI", + "name": "text-embedding-3-small", + "active": true + } + }, + { + "model": "core.embeddingmodel", + "pk": 3, + "fields": { + "provider": "GEMINI", + "name": "models/text-embedding-004", + "active": false + } + } +] \ No newline at end of file diff --git a/labs/fixtures/llm.json b/labs/fixtures/llm.json new file mode 100644 index 0000000..71e15ec --- /dev/null +++ b/labs/fixtures/llm.json @@ -0,0 +1,40 @@ +[ + { + "model": "core.llmmodel", + "pk": 1, + "fields": { + "provider": "OPENAI", + "name": "gpt-4o", + "active": true + } + }, + { + "model": "core.llmmodel", + "pk": 2, + "fields": { + "provider": "OLLAMA", + "name": "llama3.2:latest", + "max_output_tokens": 2048, + "active": false + } + }, + { + "model": "core.llmmodel", + "pk": 3, + "fields": { + "provider": "GEMINI", + "name": "gemini-2.0-flash", + "active": false + } + }, + { + "model": "core.llmmodel", + "pk": 4, + "fields": { + "provider": "ANTHROPIC", + "name": "claude-3-5-sonnet-20241022", + "max_output_tokens": 8192, + "active": false + } + } +] \ No newline at end of file diff --git a/labs/fixtures/model.json b/labs/fixtures/model.json deleted file mode 100644 index 9a82e3c..0000000 --- a/labs/fixtures/model.json +++ /dev/null @@ -1,74 +0,0 @@ -[ - { - "model": "core.model", - "pk": 1, - "fields": { - "model_type": "EMBEDDING", - "provider": "OLLAMA", - "model_name": "nomic-embed-text:latest", - "active": false - } - }, - { - "model": "core.model", - "pk": 2, - "fields": { - "model_type": "LLM", - "provider": "OPENAI", - "model_name": "gpt-4o", - "active": true - } - }, - { - "model": "core.model", - "pk": 3, - "fields": { - "model_type": "EMBEDDING", - "provider": "OPENAI", - "model_name": "text-embedding-3-small", - "active": true - } - }, - { - "model": "core.model", - "pk": 4, - "fields": { - "model_type": "LLM", - "provider": "OLLAMA", - "model_name": "llama3.2:latest", - "max_output_tokens": 2048, - "active": false - } - }, - { - "model": "core.model", - "pk": 5, - "fields": { - "model_type": "LLM", - "provider": "GEMINI", - "model_name": "gemini-2.0-flash", - "active": false - } - }, - { - "model": "core.model", - "pk": 6, - "fields": { - "model_type": "EMBEDDING", - "provider": "GEMINI", - "model_name": "models/text-embedding-004", - "active": false - } - }, - { - "model": "core.model", - "pk": 7, - "fields": { - "model_type": "LLM", - "provider": "ANTHROPIC", - "model_name": "claude-3-5-sonnet-20241022", - "max_output_tokens": 8192, - "active": false - } - } -] \ No newline at end of file diff --git a/labs/llm/anthropic.py b/labs/llm/anthropic.py index d4830fa..57e2821 100644 --- a/labs/llm/anthropic.py +++ b/labs/llm/anthropic.py @@ -1,7 +1,6 @@ import logging import os from typing import Any, Dict, List, Tuple, cast -from core.models import Model from anthropic import Anthropic from anthropic.types import Message, TextBlock @@ -10,8 +9,8 @@ class AnthropicRequester: - def __init__(self, model: Model): - self._model_name = model.model_name + def __init__(self, model): + self._model_name = model.name self._model_max_output_tokens = model.max_output_tokens api_key = os.environ.get("ANTHROPIC_API_KEY") self.client = Anthropic(api_key=api_key) diff --git a/labs/llm/gemini.py b/labs/llm/gemini.py index 6cf4fdd..2bc1899 100644 --- a/labs/llm/gemini.py +++ b/labs/llm/gemini.py @@ -1,14 +1,13 @@ import os import json from typing import List, Dict, Tuple, Any -from core.models import Model import google.generativeai as genai class GeminiRequester: - def __init__(self, model: Model): - self._model_name = model.model_name + def __init__(self, model): + self._model_name = model.name api_key = os.environ.get("GEMINI_API_KEY") genai.configure(api_key=api_key) self.generative_model = genai.GenerativeModel(self._model_name) diff --git a/labs/llm/ollama.py b/labs/llm/ollama.py index f7cca29..5781afa 100644 --- a/labs/llm/ollama.py +++ b/labs/llm/ollama.py @@ -1,12 +1,11 @@ from django.conf import settings -from core.models import Model from typing import Tuple, Dict, Any from ollama import Client class OllamaRequester: - def __init__(self, model: Model): - self._model_name = model.model_name + def __init__(self, model): + self._model_name = model.name self._model_max_output_tokens = model.max_output_tokens self._client = Client(settings.LOCAL_LLM_HOST) diff --git a/labs/llm/openai.py b/labs/llm/openai.py index f478cdf..420f101 100644 --- a/labs/llm/openai.py +++ b/labs/llm/openai.py @@ -1,5 +1,4 @@ import os -from core.models import Model from typing import Tuple, Dict, Any import openai @@ -7,8 +6,8 @@ class OpenAIRequester: - def __init__(self, model: Model): - self._model_name = model.model_name + def __init__(self, model): + self._model_name = model.name openai.api_key = os.environ.get("OPENAI_API_KEY") def completion_without_proxy(self, messages, *args, **kwargs) -> Tuple[str, Dict[str, Any]]: diff --git a/labs/tasks/llm.py b/labs/tasks/llm.py index 71005f8..ff1efab 100644 --- a/labs/tasks/llm.py +++ b/labs/tasks/llm.py @@ -4,7 +4,7 @@ from config.celery import app from config.redis_client import RedisVariable, redis_client -from core.models import Model, Project, VectorizerModel +from core.models import LLMModel, EmbeddingModel, Project, VectorizerModel from django.conf import settings from embeddings.embedder import Embedder from embeddings.vectorizers.vectorizer import Vectorizer @@ -17,7 +17,7 @@ def get_llm_response(prompt): - llm_requester, *llm_requester_args = Model.get_active_llm_model() + llm_requester, *llm_requester_args = LLMModel.get_active_model() requester = Requester(llm_requester, *llm_requester_args) retries, max_retries = 0, 5 @@ -49,7 +49,7 @@ def vectorize_repository_task(prefix="", project_id=0): if not (project_path := redis_client.get(RedisVariable.PROJECT_PATH, prefix=prefix)): project_path = Project.objects.get(id=project_id).path - embedder_class, *embeder_args = Model.get_active_embedding_model() + embedder_class, *embeder_args = EmbeddingModel.get_active_model() embedder = Embedder(embedder_class, *embeder_args) vectorizer_class = VectorizerModel.get_active_vectorizer(project_id) @@ -70,7 +70,7 @@ def find_embeddings_task( ): project_id = redis_client.get(RedisVariable.PROJECT, prefix=prefix, default=project_id) - embedder_class, *embeder_args = Model.get_active_embedding_model() + embedder_class, *embeder_args = EmbeddingModel.get_active_model() files_path = Embedder(embedder_class, *embeder_args).retrieve_files_path( redis_client.get(RedisVariable.ISSUE_BODY, prefix=prefix, default=issue_body), project_id, diff --git a/labs/tasks/logging.py b/labs/tasks/logging.py index 8548356..ec0f5c7 100644 --- a/labs/tasks/logging.py +++ b/labs/tasks/logging.py @@ -1,12 +1,12 @@ from config.celery import app from config.redis_client import RedisVariable, redis_client -from core.models import Model, WorkflowResult +from core.models import EmbeddingModel, LLMModel, WorkflowResult @app.task def save_workflow_result_task(prefix): - _, embedding_model_name = Model.get_active_embedding_model() - _, llm_model_name = Model.get_active_llm_model() + _, embedding_model_name = EmbeddingModel.get_active_model() + _, llm_model_name = LLMModel.get_active_model() project_id = redis_client.get(RedisVariable.PROJECT, prefix) embeddings = redis_client.get(RedisVariable.EMBEDDINGS, prefix) context = redis_client.get(RedisVariable.CONTEXT, prefix) diff --git a/labs/tests/conftest.py b/labs/tests/conftest.py index 1ea4ebe..8709707 100644 --- a/labs/tests/conftest.py +++ b/labs/tests/conftest.py @@ -1,7 +1,7 @@ from typing import List import pytest -from core.models import Model, ModelTypeEnum, Project, ProviderEnum, Variable +from core.models import LLMModel, EmbeddingModel, Project, ProviderEnum, Variable from embeddings.models import Embedding from tests.constants import ( ANTHROPIC_LLM_MODEL_NAME, @@ -50,10 +50,9 @@ def create_multiple_embeddings(create_test_project): @pytest.fixture @pytest.mark.django_db def create_test_ollama_embedding_config(): - return Model.objects.create( - model_type=ModelTypeEnum.EMBEDDING.name, + return EmbeddingModel.objects.create( provider=ProviderEnum.OLLAMA.name, - model_name=OLLAMA_EMBEDDING_MODEL_NAME, + name=OLLAMA_EMBEDDING_MODEL_NAME, active=True, ) @@ -61,10 +60,9 @@ def create_test_ollama_embedding_config(): @pytest.fixture @pytest.mark.django_db def create_test_ollama_llm_config(): - return Model.objects.create( - model_type=ModelTypeEnum.LLM.name, + return LLMModel.objects.create( provider=ProviderEnum.OLLAMA.name, - model_name=OLLAMA_LLM_MODEL_NAME, + name=OLLAMA_LLM_MODEL_NAME, active=True, ) @@ -72,10 +70,9 @@ def create_test_ollama_llm_config(): @pytest.fixture @pytest.mark.django_db def create_test_openai_embedding_config(): - return Model.objects.create( - model_type=ModelTypeEnum.EMBEDDING.name, + return EmbeddingModel.objects.create( provider=ProviderEnum.OPENAI.name, - model_name=OPENAI_EMBEDDING_MODEL_NAME, + name=OPENAI_EMBEDDING_MODEL_NAME, active=True, ) @@ -83,10 +80,9 @@ def create_test_openai_embedding_config(): @pytest.fixture @pytest.mark.django_db def create_test_openai_llm_config(): - return Model.objects.create( - model_type=ModelTypeEnum.LLM.name, + return LLMModel.objects.create( provider=ProviderEnum.OPENAI.name, - model_name=OPENAI_LLM_MODEL_NAME, + name=OPENAI_LLM_MODEL_NAME, active=True, ) @@ -94,10 +90,9 @@ def create_test_openai_llm_config(): @pytest.fixture @pytest.mark.django_db def create_test_gemini_embedding_config(): - return Model.objects.create( - model_type=ModelTypeEnum.EMBEDDING.name, + return EmbeddingModel.objects.create( provider=ProviderEnum.GEMINI.name, - model_name=GEMINI_EMBEDDING_MODEL_NAME, + name=GEMINI_EMBEDDING_MODEL_NAME, active=True, ) @@ -105,10 +100,9 @@ def create_test_gemini_embedding_config(): @pytest.fixture @pytest.mark.django_db def create_test_gemini_llm_config(): - return Model.objects.create( - model_type=ModelTypeEnum.LLM.name, + return LLMModel.objects.create( provider=ProviderEnum.GEMINI.name, - model_name=GEMINI_LLM_MODEL_NAME, + name=GEMINI_LLM_MODEL_NAME, active=True, ) @@ -116,9 +110,8 @@ def create_test_gemini_llm_config(): @pytest.fixture @pytest.mark.django_db def create_test_anthropic_llm_config(): - return Model.objects.create( - model_type=ModelTypeEnum.LLM.name, + return LLMModel.objects.create( provider=ProviderEnum.ANTHROPIC.name, - model_name=ANTHROPIC_LLM_MODEL_NAME, + name=ANTHROPIC_LLM_MODEL_NAME, active=True, ) diff --git a/labs/tests/test_llm.py b/labs/tests/test_llm.py index e18e1ba..c351b05 100644 --- a/labs/tests/test_llm.py +++ b/labs/tests/test_llm.py @@ -2,7 +2,7 @@ from unittest.mock import patch import pytest -from core.models import Model, VectorizerModel +from core.models import LLMModel, EmbeddingModel, VectorizerModel from embeddings.embedder import Embedder from embeddings.gemini import GeminiEmbedder from embeddings.ollama import OllamaEmbedder @@ -31,7 +31,7 @@ def call_llm_with_context(issue_summary, project): if not issue_summary: raise ValueError("issue_summary cannot be empty.") - embedder_class, *embeder_args = Model.get_active_embedding_model() + embedder_class, *embeder_args = EmbeddingModel.get_active_model() embedder = Embedder(embedder_class, *embeder_args) vectorizer_class = VectorizerModel.get_active_vectorizer(project.id) @@ -146,49 +146,49 @@ def test_local_llm_redirect( class TestLLMRequester: @pytest.mark.django_db def test_openai_llm_requester(self, create_test_openai_llm_config): - requester, model_name = Model.get_active_llm_model() + requester, model = LLMModel.get_active_model() assert issubclass(requester, OpenAIRequester) - assert model_name == OPENAI_LLM_MODEL_NAME + assert model.name == OPENAI_LLM_MODEL_NAME @pytest.mark.django_db def test_openai_embedder(self, create_test_openai_embedding_config): - embedder, model_name = Model.get_active_embedding_model() + embedder, model = EmbeddingModel.get_active_model() assert issubclass(embedder, OpenAIEmbedder) - assert model_name == OPENAI_EMBEDDING_MODEL_NAME + assert model.name == OPENAI_EMBEDDING_MODEL_NAME @pytest.mark.django_db def test_ollama_llm_requester(self, create_test_ollama_llm_config): - requester, model_name = Model.get_active_llm_model() + requester, model = LLMModel.get_active_model() assert issubclass(requester, OllamaRequester) - assert model_name == OLLAMA_LLM_MODEL_NAME + assert model.name == OLLAMA_LLM_MODEL_NAME @pytest.mark.django_db def test_ollama_embedder(self, create_test_ollama_embedding_config): - embedder, model_name = Model.get_active_embedding_model() + embedder, model = EmbeddingModel.get_active_model() assert issubclass(embedder, OllamaEmbedder) - assert model_name == OLLAMA_EMBEDDING_MODEL_NAME + assert model.name == OLLAMA_EMBEDDING_MODEL_NAME @pytest.mark.django_db def test_gemini_llm_requester(self, create_test_gemini_llm_config): - requester, model_name = Model.get_active_llm_model() + requester, model = LLMModel.get_active_model() assert issubclass(requester, GeminiRequester) - assert model_name == GEMINI_LLM_MODEL_NAME + assert model.name == GEMINI_LLM_MODEL_NAME @pytest.mark.django_db def test_gemini_embedder(self, create_test_gemini_embedding_config): - embedder, model_name = Model.get_active_embedding_model() + embedder, model = EmbeddingModel.get_active_model() assert issubclass(embedder, GeminiEmbedder) - assert model_name == GEMINI_EMBEDDING_MODEL_NAME + assert model.name == GEMINI_EMBEDDING_MODEL_NAME @pytest.mark.django_db def test_anthropic_llm_requester(self, create_test_anthropic_llm_config): - requester, model_name = Model.get_active_llm_model() + requester, model = LLMModel.get_active_model() assert issubclass(requester, AnthropicRequester) - assert model_name == ANTHROPIC_LLM_MODEL_NAME + assert model.name == ANTHROPIC_LLM_MODEL_NAME From c2cf3ed729c77841b79c29c9e35e936bf94ec4d5 Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Thu, 27 Feb 2025 10:34:19 +0000 Subject: [PATCH 03/11] _get_active_provider_model --- labs/core/forms.py | 4 ++-- labs/core/models.py | 57 +++++++++++++++++++++++++++++---------------- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/labs/core/forms.py b/labs/core/forms.py index b077f28..0d5356f 100644 --- a/labs/core/forms.py +++ b/labs/core/forms.py @@ -42,7 +42,7 @@ def clean(self): if active_count != 1: raise ValidationError( - f"You must have exactly 1 active Embedding Model, found {active_count}." + f"You must have exactly 1 active Embedding, found {active_count}." ) @@ -58,5 +58,5 @@ def clean(self): if active_count != 1: raise ValidationError( - f"You must have exactly 1 active LLM Model, found {active_count}." + f"You must have exactly 1 active LLM, found {active_count}." ) diff --git a/labs/core/models.py b/labs/core/models.py index 523e19e..cbc4980 100644 --- a/labs/core/models.py +++ b/labs/core/models.py @@ -1,6 +1,6 @@ import os from enum import Enum -from typing import Literal, Tuple +from typing import Tuple from django.core.exceptions import ValidationError from django.db import models @@ -25,7 +25,10 @@ "ANTHROPIC": {"llm": AnthropicRequester}, } -vectorizer_model_class = {"CHUNK_VECTORIZER": ChunkVectorizer, "PYTHON_VECTORIZER": PythonVectorizer} +vectorizer_model_class = { + "CHUNK_VECTORIZER": ChunkVectorizer, + "PYTHON_VECTORIZER": PythonVectorizer, +} class ProviderEnum(Enum): NO_PROVIDER = "No provider" @@ -81,7 +84,7 @@ def _default_vectorizer_value_validation(self): f"The only possible values for DEFAULT_VECTORIZER are: {', '.join(allowed_vectorizer_values)}" ) - def __str__(self): + def __str__(self) -> str: return self.name @@ -98,11 +101,16 @@ def get_active_model(cls) -> Tuple[Embedder, "EmbeddingModel"]: raise ValueError("No active embedding model configured") Variable.load_provider_keys(model.provider) - embedder_class = provider_model_class[model.provider]["embedding"] + + try: + embedder_class = provider_model_class[model.provider]["embedding"] + except KeyError: + raise ValueError(f"Provider '{model.provider}' is missing or has no embedder class defined.") + return embedder_class, model - def __str__(self): - return f"EmbeddingModel {self.provider} {self.name}" + def __str__(self) -> str: + return f"EmbeddingModel [{self.provider}] {self.name}" class Meta: verbose_name = "Embedding" @@ -131,11 +139,16 @@ def get_active_model(cls) -> Tuple[Requester, "LLMModel"]: raise ValueError("No active llm model configured") Variable.load_provider_keys(model.provider) - llm_class = provider_model_class[model.provider]["llm"] + + try: + llm_class = provider_model_class[model.provider]["llm"] + except KeyError: + raise ValueError(f"Provider '{model.provider}' is missing or has no LLM class defined.") + return llm_class, model - def __str__(self): - return f"LLMModel {self.provider} {self.name}" + def __str__(self) -> str: + return f"LLMModel [{self.provider}] {self.name}" class Meta: verbose_name = "LLM" @@ -170,7 +183,7 @@ def save(self, *args, **kwargs): VectorizerModel.objects.create(project=self) Prompt.objects.create(project=self) - def __str__(self): + def __str__(self) -> str: return self.name class Meta: @@ -183,16 +196,20 @@ class VectorizerModel(models.Model): vectorizer_type = models.CharField(choices=VectorizerEnum.choices(), default=Variable.get_default_vectorizer_value) @staticmethod - def get_active_vectorizer(project_id) -> Vectorizer: - queryset = VectorizerModel.objects.filter(project__id=project_id) - if not queryset.exists(): - raise ValueError("No vectorizer configured") + def get_active_vectorizer(project_id: int) -> Vectorizer: + vector_model = VectorizerModel.objects.filter(project__id=project_id).first() + if not vector_model: + raise ValueError("No vectorizer configured for this project.") + + try: + vec_class = vectorizer_model_class[vector_model.vectorizer_type] + except KeyError: + raise ValueError(f"Unrecognized vectorizer type '{vector_model.vectorizer_type}'") - vectorizer_model = queryset.first() - return vectorizer_model_class[vectorizer_model.vectorizer_type] + return vec_class - def __str__(self): - return self.vectorizer_type + def __str__(self) -> str: + return f"{self.vectorizer_type} for {self.project.name}" class Meta: verbose_name = "Vectorizer" @@ -211,7 +228,7 @@ class WorkflowResult(models.Model): pre_commit_error = models.TextField(null=True) created_at = models.DateTimeField(auto_now_add=True) - def __str__(self): + def __str__(self) -> str: return f"{self.task_id}" class Meta: @@ -252,7 +269,7 @@ def get_instruction(project_id: int) -> str: return queryset.first().instruction - def __str__(self): + def __str__(self) -> str: return f"{self.persona[:50]}..., {self.instruction[:50]}..." class Meta: From c36e9cd361f6cad06c9a3d77b2661f7956079f89 Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Thu, 27 Feb 2025 11:27:50 +0000 Subject: [PATCH 04/11] migrations --- labs/core/admin.py | 4 +- ...le_value_alter_variable_unique_together.py | 22 --------- ...ingmodel_llmmodel_delete_model_and_more.py | 48 ++++++++++++++++--- labs/core/models.py | 15 ++++-- 4 files changed, 54 insertions(+), 35 deletions(-) delete mode 100644 labs/core/migrations/0008_alter_variable_value_alter_variable_unique_together.py diff --git a/labs/core/admin.py b/labs/core/admin.py index 0a76dca..6fbed17 100644 --- a/labs/core/admin.py +++ b/labs/core/admin.py @@ -12,7 +12,7 @@ class EmbeddingModelAdmin(admin.ModelAdmin): list_display = ("id", "provider", "name", "active") list_display_links = ("id",) - list_editable = ("provider", "name", "active") + list_editable = ("name", "active") list_filter = ("provider", "name") search_fields = ("provider", "name") @@ -31,7 +31,7 @@ def get_changelist_formset(self, request, **kwargs): class LLMModelAdmin(admin.ModelAdmin): list_display = ("id", "provider", "name", "max_output_tokens", "active") list_display_links = ("id",) - list_editable = ("provider", "name", "max_output_tokens", "active") + list_editable = ("name", "max_output_tokens", "active") list_filter = ("provider", "name") search_fields = ("provider", "name", "max_output_tokens") diff --git a/labs/core/migrations/0008_alter_variable_value_alter_variable_unique_together.py b/labs/core/migrations/0008_alter_variable_value_alter_variable_unique_together.py deleted file mode 100644 index 42c352d..0000000 --- a/labs/core/migrations/0008_alter_variable_value_alter_variable_unique_together.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.1.5 on 2025-02-25 11:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0007_alter_model_provider_alter_variable_provider"), - ] - - operations = [ - migrations.AlterField( - model_name="variable", - name="value", - field=models.TextField(blank=True, null=True), - ), - migrations.AlterUniqueTogether( - name="variable", - unique_together={("provider", "name")}, - ), - ] diff --git a/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py b/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py index 022d0d4..b6b8728 100644 --- a/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py +++ b/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-02-26 17:36 +# Generated by Django 5.1.5 on 2025-02-27 11:23 from django.db import migrations, models @@ -34,8 +34,19 @@ class Migration(migrations.Migration): ] ), ), - ("name", models.CharField(max_length=255)), - ("active", models.BooleanField(default=True)), + ( + "name", + models.CharField( + help_text="Ensure this Embedding exists and is downloaded.", + max_length=255, + ), + ), + ( + "active", + models.BooleanField( + default=True, help_text="Only one Embedding can be active." + ), + ), ], options={ "verbose_name": "Embedding", @@ -66,11 +77,27 @@ class Migration(migrations.Migration): ] ), ), - ("name", models.CharField(max_length=255)), - ("active", models.BooleanField(default=True)), + ( + "name", + models.CharField( + help_text="Ensure this LLM exists and is downloaded.", + max_length=255, + ), + ), + ( + "active", + models.BooleanField( + default=True, help_text="Only one LLM can be active." + ), + ), ( "max_output_tokens", - models.IntegerField(blank=True, default=None, null=True), + models.IntegerField( + blank=True, + default=None, + help_text="Leave blank for auto-detection, set only if required.", + null=True, + ), ), ], options={ @@ -81,6 +108,15 @@ class Migration(migrations.Migration): migrations.DeleteModel( name="Model", ), + migrations.AlterField( + model_name="variable", + name="value", + field=models.TextField(blank=True, null=True), + ), + migrations.AlterUniqueTogether( + name="variable", + unique_together={("provider", "name")}, + ), migrations.AddIndex( model_name="embeddingmodel", index=models.Index( diff --git a/labs/core/models.py b/labs/core/models.py index fe6a749..c77eb71 100644 --- a/labs/core/models.py +++ b/labs/core/models.py @@ -93,8 +93,8 @@ class Meta: class EmbeddingModel(models.Model): provider = models.CharField(choices=ProviderEnum.choices()) - name = models.CharField(max_length=255) - active = models.BooleanField(default=True) + name = models.CharField(max_length=255,help_text="Ensure this Embedding exists and is downloaded.") + active = models.BooleanField(default=True, help_text="Only one Embedding can be active.") @classmethod def get_active_model(cls) -> Tuple[Embedder, "EmbeddingModel"]: @@ -130,9 +130,14 @@ class Meta: class LLMModel(models.Model): provider = models.CharField(choices=ProviderEnum.choices()) - name = models.CharField(max_length=255) - active = models.BooleanField(default=True) - max_output_tokens = models.IntegerField(null=True, blank=True, default=None) + name = models.CharField(max_length=255, help_text="Ensure this LLM exists and is downloaded.") + active = models.BooleanField(default=True, help_text="Only one LLM can be active.") + max_output_tokens = models.IntegerField( + null=True, + blank=True, + default=None, + help_text="Leave blank for auto-detection, set only if required." + ) @classmethod def get_active_model(cls) -> Tuple[Requester, "LLMModel"]: From 2620e10d554022b1c611dce049e1ac273da46542 Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Thu, 27 Feb 2025 11:28:36 +0000 Subject: [PATCH 05/11] keep provider editable --- labs/core/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/labs/core/admin.py b/labs/core/admin.py index 6fbed17..0a76dca 100644 --- a/labs/core/admin.py +++ b/labs/core/admin.py @@ -12,7 +12,7 @@ class EmbeddingModelAdmin(admin.ModelAdmin): list_display = ("id", "provider", "name", "active") list_display_links = ("id",) - list_editable = ("name", "active") + list_editable = ("provider", "name", "active") list_filter = ("provider", "name") search_fields = ("provider", "name") @@ -31,7 +31,7 @@ def get_changelist_formset(self, request, **kwargs): class LLMModelAdmin(admin.ModelAdmin): list_display = ("id", "provider", "name", "max_output_tokens", "active") list_display_links = ("id",) - list_editable = ("name", "max_output_tokens", "active") + list_editable = ("provider", "name", "max_output_tokens", "active") list_filter = ("provider", "name") search_fields = ("provider", "name", "max_output_tokens") From c3b16042165455ac9b52a5065d23ee62149936b6 Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Thu, 27 Feb 2025 11:35:58 +0000 Subject: [PATCH 06/11] keep or anthropic will show on embedding even if it only has llm defined --- labs/core/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/labs/core/admin.py b/labs/core/admin.py index 0a76dca..6fbed17 100644 --- a/labs/core/admin.py +++ b/labs/core/admin.py @@ -12,7 +12,7 @@ class EmbeddingModelAdmin(admin.ModelAdmin): list_display = ("id", "provider", "name", "active") list_display_links = ("id",) - list_editable = ("provider", "name", "active") + list_editable = ("name", "active") list_filter = ("provider", "name") search_fields = ("provider", "name") @@ -31,7 +31,7 @@ def get_changelist_formset(self, request, **kwargs): class LLMModelAdmin(admin.ModelAdmin): list_display = ("id", "provider", "name", "max_output_tokens", "active") list_display_links = ("id",) - list_editable = ("provider", "name", "max_output_tokens", "active") + list_editable = ("name", "max_output_tokens", "active") list_filter = ("provider", "name") search_fields = ("provider", "name", "max_output_tokens") From e4cfde069a575559eac4d7f74bb87a9a7f99e805 Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Thu, 27 Feb 2025 12:06:11 +0000 Subject: [PATCH 07/11] correct test --- labs/tests/database/test_embeddings.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/labs/tests/database/test_embeddings.py b/labs/tests/database/test_embeddings.py index 8c6ebea..cc71eb9 100644 --- a/labs/tests/database/test_embeddings.py +++ b/labs/tests/database/test_embeddings.py @@ -6,6 +6,7 @@ from embeddings.embedder import Embedder, Embeddings from embeddings.models import Embedding from embeddings.openai import OpenAIEmbedder +from core.factories import EmbeddingModelFactory from tests.constants import MULTIPLE_EMBEDDINGS, OPENAI_EMBEDDING_MODEL_NAME, SINGLE_EMBEDDING @@ -56,7 +57,13 @@ def test_reembed_code(create_test_project): ], ) - Embedder(OpenAIEmbedder, model=OPENAI_EMBEDDING_MODEL_NAME).reembed_code( + embedding_model = EmbeddingModelFactory( + provider="OPENAI", + name=OPENAI_EMBEDDING_MODEL_NAME, + active=True, + ) + + Embedder(OpenAIEmbedder, model=embedding_model).reembed_code( project_id=project.id, files_texts=files_texts, embeddings=embeddings, From c60c6bf3ff02fd18dc92a7cdb5a750f19a5e90e3 Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Thu, 27 Feb 2025 12:25:27 +0000 Subject: [PATCH 08/11] remove unused import --- Makefile | 3 +++ .../migrations/0006_model_unique_active_embedding_and_more.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a724b39..3e7fcce 100644 --- a/Makefile +++ b/Makefile @@ -71,6 +71,9 @@ migrations: migrate: poetry run python labs/manage.py migrate +tests: + poetry run pytest ./labs + createuser: DJANGO_SUPERUSER_PASSWORD=admin poetry run python labs/manage.py createsuperuser --noinput --username=admin --email=admin@example.com diff --git a/labs/core/migrations/0006_model_unique_active_embedding_and_more.py b/labs/core/migrations/0006_model_unique_active_embedding_and_more.py index c1f57f1..71c9125 100644 --- a/labs/core/migrations/0006_model_unique_active_embedding_and_more.py +++ b/labs/core/migrations/0006_model_unique_active_embedding_and_more.py @@ -1,6 +1,5 @@ # Generated by Django 5.1.5 on 2025-02-21 10:50 -import core.models from django.db import migrations, models From fd21d72b6636dd3740a0d31cd70e4ba8175796b7 Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Thu, 27 Feb 2025 12:57:41 +0000 Subject: [PATCH 09/11] revert makefile commit --- Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Makefile b/Makefile index 3e7fcce..a724b39 100644 --- a/Makefile +++ b/Makefile @@ -71,9 +71,6 @@ migrations: migrate: poetry run python labs/manage.py migrate -tests: - poetry run pytest ./labs - createuser: DJANGO_SUPERUSER_PASSWORD=admin poetry run python labs/manage.py createsuperuser --noinput --username=admin --email=admin@example.com From 4412a35421142567ae4527413c997c3971265ab2 Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Fri, 28 Feb 2025 10:41:05 +0000 Subject: [PATCH 10/11] refactor --- labs/core/forms.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/labs/core/forms.py b/labs/core/forms.py index 0d5356f..3c61219 100644 --- a/labs/core/forms.py +++ b/labs/core/forms.py @@ -30,33 +30,22 @@ class Meta: fields = ["name", "description", "path", "url"] -class EmbeddingModelFormSet(BaseModelFormSet): +class SingleActiveFormSet(BaseModelFormSet): + model_name = "Model" + def clean(self): super().clean() - active_count = sum( - 1 - for form in self.forms + 1 for form in self.forms if form.cleaned_data and form.cleaned_data.get("active", False) ) - if active_count != 1: raise ValidationError( - f"You must have exactly 1 active Embedding, found {active_count}." + f"You must have exactly 1 active {self.model_name}, found {active_count}." ) +class EmbeddingModelFormSet(SingleActiveFormSet): + model_name = "Embedding" -class LLMModelFormSet(BaseModelFormSet): - def clean(self): - super().clean() - - active_count = sum( - 1 - for form in self.forms - if form.cleaned_data and form.cleaned_data.get("active", False) - ) - - if active_count != 1: - raise ValidationError( - f"You must have exactly 1 active LLM, found {active_count}." - ) +class LLMModelFormSet(SingleActiveFormSet): + model_name = "LLM" \ No newline at end of file From 52f517ff00f97e7e2ea0b753565e54c9da85c826 Mon Sep 17 00:00:00 2001 From: Andre Carvalho Date: Fri, 28 Feb 2025 11:16:00 +0000 Subject: [PATCH 11/11] migrations corrected --- ...ingmodel_llmmodel_delete_model_and_more.py | 128 +--------------- ...ingmodel_llmmodel_delete_model_and_more.py | 139 ++++++++++++++++++ 2 files changed, 140 insertions(+), 127 deletions(-) create mode 100644 labs/core/migrations/0009_embeddingmodel_llmmodel_delete_model_and_more.py diff --git a/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py b/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py index b6b8728..42c352d 100644 --- a/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py +++ b/labs/core/migrations/0008_embeddingmodel_llmmodel_delete_model_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.5 on 2025-02-27 11:23 +# Generated by Django 5.1.5 on 2025-02-25 11:20 from django.db import migrations, models @@ -10,104 +10,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name="EmbeddingModel", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "provider", - models.CharField( - choices=[ - ("NO_PROVIDER", "No provider"), - ("OPENAI", "OpenAI"), - ("OLLAMA", "Ollama"), - ("GEMINI", "Gemini"), - ("ANTHROPIC", "Anthropic"), - ] - ), - ), - ( - "name", - models.CharField( - help_text="Ensure this Embedding exists and is downloaded.", - max_length=255, - ), - ), - ( - "active", - models.BooleanField( - default=True, help_text="Only one Embedding can be active." - ), - ), - ], - options={ - "verbose_name": "Embedding", - "verbose_name_plural": "Embeddings", - }, - ), - migrations.CreateModel( - name="LLMModel", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "provider", - models.CharField( - choices=[ - ("NO_PROVIDER", "No provider"), - ("OPENAI", "OpenAI"), - ("OLLAMA", "Ollama"), - ("GEMINI", "Gemini"), - ("ANTHROPIC", "Anthropic"), - ] - ), - ), - ( - "name", - models.CharField( - help_text="Ensure this LLM exists and is downloaded.", - max_length=255, - ), - ), - ( - "active", - models.BooleanField( - default=True, help_text="Only one LLM can be active." - ), - ), - ( - "max_output_tokens", - models.IntegerField( - blank=True, - default=None, - help_text="Leave blank for auto-detection, set only if required.", - null=True, - ), - ), - ], - options={ - "verbose_name": "LLM", - "verbose_name_plural": "LLMs", - }, - ), - migrations.DeleteModel( - name="Model", - ), migrations.AlterField( model_name="variable", name="value", @@ -117,32 +19,4 @@ class Migration(migrations.Migration): name="variable", unique_together={("provider", "name")}, ), - migrations.AddIndex( - model_name="embeddingmodel", - index=models.Index( - fields=["provider", "name"], name="core_embedd_provide_04143f_idx" - ), - ), - migrations.AddConstraint( - model_name="embeddingmodel", - constraint=models.UniqueConstraint( - condition=models.Q(("active", True)), - fields=("provider",), - name="unique_active_embedding", - ), - ), - migrations.AddIndex( - model_name="llmmodel", - index=models.Index( - fields=["provider", "name"], name="core_llmmod_provide_8330c3_idx" - ), - ), - migrations.AddConstraint( - model_name="llmmodel", - constraint=models.UniqueConstraint( - condition=models.Q(("active", True)), - fields=("provider",), - name="unique_active_llm", - ), - ), ] diff --git a/labs/core/migrations/0009_embeddingmodel_llmmodel_delete_model_and_more.py b/labs/core/migrations/0009_embeddingmodel_llmmodel_delete_model_and_more.py new file mode 100644 index 0000000..8d5ce16 --- /dev/null +++ b/labs/core/migrations/0009_embeddingmodel_llmmodel_delete_model_and_more.py @@ -0,0 +1,139 @@ +# Generated by Django 5.1.5 on 2025-02-28 11:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0008_embeddingmodel_llmmodel_delete_model_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="EmbeddingModel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "provider", + models.CharField( + choices=[ + ("NO_PROVIDER", "No provider"), + ("OPENAI", "OpenAI"), + ("OLLAMA", "Ollama"), + ("GEMINI", "Gemini"), + ("ANTHROPIC", "Anthropic"), + ] + ), + ), + ( + "name", + models.CharField( + help_text="Ensure this Embedding exists and is downloaded.", + max_length=255, + ), + ), + ( + "active", + models.BooleanField( + default=True, help_text="Only one Embedding can be active." + ), + ), + ], + options={ + "verbose_name": "Embedding", + "verbose_name_plural": "Embeddings", + }, + ), + migrations.CreateModel( + name="LLMModel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "provider", + models.CharField( + choices=[ + ("NO_PROVIDER", "No provider"), + ("OPENAI", "OpenAI"), + ("OLLAMA", "Ollama"), + ("GEMINI", "Gemini"), + ("ANTHROPIC", "Anthropic"), + ] + ), + ), + ( + "name", + models.CharField( + help_text="Ensure this LLM exists and is downloaded.", + max_length=255, + ), + ), + ( + "active", + models.BooleanField( + default=True, help_text="Only one LLM can be active." + ), + ), + ( + "max_output_tokens", + models.IntegerField( + blank=True, + default=None, + help_text="Leave blank for auto-detection, set only if required.", + null=True, + ), + ), + ], + options={ + "verbose_name": "LLM", + "verbose_name_plural": "LLMs", + }, + ), + migrations.DeleteModel( + name="Model", + ), + migrations.AddIndex( + model_name="embeddingmodel", + index=models.Index( + fields=["provider", "name"], name="core_embedd_provide_04143f_idx" + ), + ), + migrations.AddConstraint( + model_name="embeddingmodel", + constraint=models.UniqueConstraint( + condition=models.Q(("active", True)), + fields=("provider",), + name="unique_active_embedding", + ), + ), + migrations.AddIndex( + model_name="llmmodel", + index=models.Index( + fields=["provider", "name"], name="core_llmmod_provide_8330c3_idx" + ), + ), + migrations.AddConstraint( + model_name="llmmodel", + constraint=models.UniqueConstraint( + condition=models.Q(("active", True)), + fields=("provider",), + name="unique_active_llm", + ), + ), + ]