Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions learning_resources/converters/opendataloader_llm_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@

log = logging.getLogger(__name__)

# drop unsupported model params
litellm.drop_params = True

# --- Configuration ---
MIN_IMAGE_DIMENSION = 32
MIN_IMAGE_RATIO = 12
Expand Down
7 changes: 4 additions & 3 deletions learning_resources/etl/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@
OfferedBy,
RunStatus,
)
from learning_resources.converters.opendataloader_llm_converter import (
OpenDataLoaderLLMConverter,
)
from learning_resources.etl.constants import (
RESOURCE_DELIVERY_MAPPING,
TIME_INTERVAL_MAPPING,
Expand Down Expand Up @@ -1294,6 +1291,10 @@ def _pdf_to_markdown(pdf_path):
"""
Convert a PDF file to markdown using an llm
"""
from learning_resources.converters.opendataloader_llm_converter import (
OpenDataLoaderLLMConverter,
)

converter = OpenDataLoaderLLMConverter(pdf_path)
return converter.convert_to_markdown()

Expand Down
3 changes: 2 additions & 1 deletion learning_resources/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from django.db.models import F, Max, Prefetch, Q
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
from isodate import parse_duration
from langchain_text_splitters import RecursiveJsonSplitter
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

Expand Down Expand Up @@ -1128,6 +1127,8 @@ def render_document(self):

def render_chunks(self):
"""Convert resource info to markdown chunks"""
from langchain_text_splitters import RecursiveJsonSplitter

rendered_doc = self.render_document()
resource_type = self.instance.get("resource_type", "course")
"""
Expand Down
3 changes: 2 additions & 1 deletion learning_resources/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from django.utils import timezone

from learning_resources.constants import LearningResourceType
from learning_resources.content_summarizer import ContentSummarizer
from learning_resources.etl import ovs, pipelines, youtube
from learning_resources.etl.canvas import (
sync_canvas_archive,
Expand Down Expand Up @@ -549,6 +548,8 @@ def summarize_content_files_task(
Returns:
- None
"""
from learning_resources.content_summarizer import ContentSummarizer
Comment thread
blarghmatey marked this conversation as resolved.

summarizer = ContentSummarizer()
return summarizer.summarize_content_files_by_ids(content_file_ids, overwrite)

Expand Down
2 changes: 1 addition & 1 deletion learning_resources/tasks_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ def test_summarize_unprocessed_content(
"learning_resources.tasks.summarize_content_files_task", autospec=True
)
get_unprocessed_content_file_ids_mock = mocker.patch(
"learning_resources.tasks.ContentSummarizer.get_unprocessed_content_file_ids",
"learning_resources.content_summarizer.ContentSummarizer.get_unprocessed_content_file_ids",
autospec=True,
return_value=ids,
)
Expand Down
3 changes: 2 additions & 1 deletion learning_resources/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import html2text
import rapidjson
import requests
import tiktoken
import yaml
from botocore.exceptions import ClientError
from django.conf import settings
Expand Down Expand Up @@ -800,6 +799,8 @@ def truncate_to_tokens(text: str, max_tokens: int, model: str = "gpt-4o") -> str
"""
Truncate text to a maximum number of tokens for a given model.
"""
import tiktoken

encoding = tiktoken.encoding_for_model(model)
tokens = encoding.encode(text)
if len(tokens) <= max_tokens:
Expand Down
9 changes: 9 additions & 0 deletions main/sentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.redis import RedisIntegration

# these errors occur when a shutdown is happening (usually caused by a SIGTERM)
SHUTDOWN_ERRORS = (WorkerLostError, SystemExit)
Expand Down Expand Up @@ -73,9 +74,17 @@ def init_sentry( # noqa: PLR0913
before_send=before_send,
traces_sample_rate=traces_sample_rate,
profiles_sample_rate=profiles_sample_rate,
# Sentry's auto-enabling integrations (langchain, boto3, openai, etc.)
# import their target library at init time just to check whether it's
# installed, regardless of whether it's ever used -- e.g. langchain is
# installed as a litellm transitive dependency but never used directly,
# and importing its Sentry integration alone pulls in the whole
# langchain/tiktoken stack on every process. Opt in explicitly instead.
auto_enabling_integrations=False,
integrations=[
DjangoIntegration(),
CeleryIntegration(),
LoggingIntegration(level=log_level),
RedisIntegration(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add Boto3Integration and HttpxIntegration here too, if they don't add much memory usage?

Might want to double-check with @shanbady about whether or not sentry's LangchainIntegration ever proved useful here for debugging problems.

],
)
3 changes: 3 additions & 0 deletions vector_search/encoders/litellm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
log = logging.getLogger()
redis_url = urlparse(settings.CELERY_BROKER_URL)

# drop unsupported model params
litellm.drop_params = True

# these must be set directly via environ (litellm limitation)
os.environ["REDIS_SSL"] = str(redis_url.scheme.endswith("ss"))
litellm.cache = Cache(
Expand Down
6 changes: 4 additions & 2 deletions vector_search/encoders/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import logging
from functools import cache

import tiktoken
from django.conf import settings
from django.utils.module_loading import import_string
from litellm import get_model_info

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -74,6 +72,8 @@ def truncate_to_model_limit(
Returns:
Truncated text.
"""
from litellm import get_model_info

max_input_tokens = None

try:
Expand All @@ -84,6 +84,8 @@ def truncate_to_model_limit(

if max_input_tokens and token_encoding_name:
try:
import tiktoken

encoding = tiktoken.get_encoding(token_encoding_name)
tokens = encoding.encode(text)

Expand Down
30 changes: 14 additions & 16 deletions vector_search/encoders/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ def warning_mock(mocker):
def test_truncate_to_model_limit_truncates_long_text(mocker, warning_mock):
"""Text over the model's max input tokens is cut to that token limit"""
mocker.patch(
"vector_search.encoders.utils.get_model_info",
"litellm.get_model_info",
return_value={"max_input_tokens": 5},
)
mocker.patch(
"vector_search.encoders.utils.tiktoken.get_encoding",
"tiktoken.get_encoding",
return_value=FakeEncoding(),
)

Expand All @@ -55,11 +55,11 @@ def test_truncate_to_model_limit_truncates_long_text(mocker, warning_mock):
def test_truncate_to_model_limit_leaves_short_text_unchanged(mocker, warning_mock):
"""Text within the model's max input tokens is returned as-is"""
mocker.patch(
"vector_search.encoders.utils.get_model_info",
"litellm.get_model_info",
return_value={"max_input_tokens": 5},
)
mocker.patch(
"vector_search.encoders.utils.tiktoken.get_encoding",
"tiktoken.get_encoding",
return_value=FakeEncoding(),
)

Expand All @@ -78,12 +78,10 @@ def test_truncate_to_model_limit_unknown_model_falls_back_to_chars(
):
"""Fall back to the character limit when model info can't be determined"""
mocker.patch(
"vector_search.encoders.utils.get_model_info",
"litellm.get_model_info",
side_effect=ValueError("unknown model"),
)
get_encoding_mock = mocker.patch(
"vector_search.encoders.utils.tiktoken.get_encoding"
)
get_encoding_mock = mocker.patch("tiktoken.get_encoding")
text = "x" * (DEFAULT_TRUNCATE_CHARS + 100)

result = truncate_to_model_limit(
Expand All @@ -102,7 +100,7 @@ def test_truncate_to_model_limit_missing_max_tokens_falls_back_to_chars(
):
"""Fall back to the character limit when the model has no max_input_tokens"""
mocker.patch(
"vector_search.encoders.utils.get_model_info",
"litellm.get_model_info",
return_value={},
)
text = "x" * (DEFAULT_TRUNCATE_CHARS + 100)
Expand All @@ -122,7 +120,7 @@ def test_truncate_to_model_limit_no_token_encoding_falls_back_to_chars(
):
"""Fall back to the character limit when no token encoding name is given"""
mocker.patch(
"vector_search.encoders.utils.get_model_info",
"litellm.get_model_info",
return_value={"max_input_tokens": 5},
)
text = "x" * (DEFAULT_TRUNCATE_CHARS + 100)
Expand All @@ -136,11 +134,11 @@ def test_truncate_to_model_limit_no_token_encoding_falls_back_to_chars(
def test_truncate_to_model_limit_bad_encoding_falls_back_to_chars(mocker, warning_mock):
"""Fall back to the character limit when the tiktoken encoding fails"""
mocker.patch(
"vector_search.encoders.utils.get_model_info",
"litellm.get_model_info",
return_value={"max_input_tokens": 5},
)
mocker.patch(
"vector_search.encoders.utils.tiktoken.get_encoding",
"tiktoken.get_encoding",
side_effect=ValueError("bad encoding"),
)
text = "x" * (DEFAULT_TRUNCATE_CHARS + 100)
Expand All @@ -158,7 +156,7 @@ def test_truncate_to_model_limit_bad_encoding_falls_back_to_chars(mocker, warnin
def test_truncate_to_model_limit_fallback_warns_once_per_model(mocker, warning_mock):
"""The fallback warning is deduplicated per model/encoding combination"""
mocker.patch(
"vector_search.encoders.utils.get_model_info",
"litellm.get_model_info",
side_effect=ValueError("unknown model"),
)

Expand All @@ -183,7 +181,7 @@ def test_truncate_to_model_limit_strict_raises_instead_of_falling_back(
):
"""In strict mode any fallback condition raises instead of truncating"""
mocker.patch(
"vector_search.encoders.utils.get_model_info",
"litellm.get_model_info",
**model_info_kwargs,
)
# no token_encoding_name, so even a known model can't use token truncation
Expand All @@ -195,11 +193,11 @@ def test_truncate_to_model_limit_strict_raises_instead_of_falling_back(
def test_truncate_to_model_limit_strict_allows_token_truncation(mocker, warning_mock):
"""Strict mode doesn't interfere when token-based truncation works"""
mocker.patch(
"vector_search.encoders.utils.get_model_info",
"litellm.get_model_info",
return_value={"max_input_tokens": 5},
)
mocker.patch(
"vector_search.encoders.utils.tiktoken.get_encoding",
"tiktoken.get_encoding",
return_value=FakeEncoding(),
)

Expand Down
3 changes: 2 additions & 1 deletion vector_search/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from django.core.cache import caches
from django.db.models import Q

from learning_resources.content_summarizer import ContentSummarizer
from learning_resources.models import (
ContentFile,
Course,
Expand Down Expand Up @@ -741,6 +740,8 @@ def _missing_summaries():
# of letting it scan every learning resource.
return []

from learning_resources.content_summarizer import ContentSummarizer

summarizer = ContentSummarizer()
return summarizer.get_unprocessed_content_file_ids(
overwrite=False,
Expand Down
11 changes: 6 additions & 5 deletions vector_search/utils.py
Comment thread
blarghmatey marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,11 @@
from django.conf import settings
from django.db import close_old_connections
from django.db.models import Prefetch, Q
from langchain_text_splitters import (
MarkdownHeaderTextSplitter,
RecursiveCharacterTextSplitter,
)
from qdrant_client import AsyncQdrantClient, QdrantClient, models

from learning_resources.constants import (
PROGRAM_COURSE_CACHE_KEY_TEST_MODE,
)
from learning_resources.content_summarizer import ContentSummarizer
from learning_resources.models import (
ContentFile,
LearningResource,
Expand Down Expand Up @@ -393,6 +388,8 @@ def embed_topics():

@cache
def _get_text_splitter(**kwargs):
from langchain_text_splitters import RecursiveCharacterTextSplitter
Comment thread
blarghmatey marked this conversation as resolved.

if settings.LITELLM_TOKEN_ENCODING_NAME:
kwargs["encoding_name"] = settings.LITELLM_TOKEN_ENCODING_NAME
return RecursiveCharacterTextSplitter.from_tiktoken_encoder(**kwargs)
Expand Down Expand Up @@ -429,6 +426,8 @@ def _chunk_markdown_documents(text, metadata):
metadata so every chunk's page_content is self-describing
for embedding.
"""
from langchain_text_splitters import MarkdownHeaderTextSplitter

header_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=MARKDOWN_HEADERS_TO_SPLIT_ON,
strip_headers=False,
Expand Down Expand Up @@ -893,6 +892,8 @@ def _summarize_content_files_for_embedding(
if not fill_summary_content_ids and not changed_summary_content_ids:
return docs_batch

from learning_resources.content_summarizer import ContentSummarizer

summarizer = ContentSummarizer()
if fill_summary_content_ids:
summarizer.summarize_content_files_by_ids(
Expand Down
10 changes: 7 additions & 3 deletions vector_search/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ def test_document_chunker_tiktoken(mocker):
encoder = dense_encoder()
encoder.token_encoding_name = None
mocked_splitter = mocker.patch(
"vector_search.utils.RecursiveCharacterTextSplitter.from_tiktoken_encoder"
"langchain_text_splitters.RecursiveCharacterTextSplitter.from_tiktoken_encoder"
)

_chunk_documents(["this is a test document"], [{}])
Expand All @@ -628,11 +628,15 @@ def test_text_splitter_chunk_size_override(mocker):
settings.CONTENT_FILE_EMBEDDING_CHUNK_SIZE_OVERRIDE = chunk_size
settings.CONTENT_FILE_EMBEDDING_CHUNK_OVERLAP = chunk_size / 10
encoder = dense_encoder()
mocked_splitter = mocker.patch("vector_search.utils.RecursiveCharacterTextSplitter")
mocked_splitter = mocker.patch(
"langchain_text_splitters.RecursiveCharacterTextSplitter"
)
encoder.token_encoding_name = "cl100k_base" # noqa: S105
_chunk_documents(["this is a test document"], [{}])
assert mocked_splitter.mock_calls[0].kwargs["chunk_size"] == 100
mocked_splitter = mocker.patch("vector_search.utils.RecursiveCharacterTextSplitter")
mocked_splitter = mocker.patch(
"langchain_text_splitters.RecursiveCharacterTextSplitter"
)
settings.CONTENT_FILE_EMBEDDING_CHUNK_SIZE_OVERRIDE = None
_chunk_documents(["this is a test document"], [{}])
assert "chunk_size" not in mocked_splitter.mock_calls[0].kwargs
Expand Down
Loading