Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: Bug Report
about: Create a report to help us improve
title: ""
labels: ["bug"]
assignees: []
---

<!-- Rest of the template body here -->


## What's the problem?
...

## Steps to Reproduce
...

## Screenshot
If applicable.

## Proposed Solution
...

## Notes
...
16 changes: 16 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: Enhancement Request
about: Ask for a change.
title: ""
labels: ["enhancement"]
assignees: []
---

## Desired Change
...

## Rationale
...

## Notes
...
10 changes: 10 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Description
...

## Summary of Changes
...

Use ```Closes #X``` to automatically close an issue addressed by this PR.

# Proof it Works
...
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "llm" # The pip install <name>.
version = "0.4.0"
version = "0.4.1"
description = "Library for easy use of LLMs."
readme = "README.md"
authors = [
Expand Down Expand Up @@ -48,7 +48,7 @@ explicit = true # Only used for torch packages explicitly mapped below.
[tool.uv.sources]
torch = { index = "pytorch-cu130" }
torchvision = { index = "pytorch-cu130" }
llm-conversation = { git = "https://github.com/EricApgar/llm-conversation", rev = "v0.2.0" }
llm-conversation = { git = "https://github.com/EricApgar/llm-conversation" }

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
12 changes: 6 additions & 6 deletions src/llm/other/demo.py → scripts/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@

if __name__ == '__main__':

# model = GptOss20b()
model = GptOss20b()
model_cache_dir = input(prompt='Enter model cache dir: ')
model.load(location=r'/home/eric/Repos/model_cache')

c = Conversation()
c.set_overall_prompt(text='Your name is Seamus OFinnegan. Respond as the assistant in character.')
c.add_context(text='Youre a real salt of the earth Irish coal miner.')
c.add_context(text='You get straight to the point and dont waste words on small talk.')
c.add_context(text='You are married to your wife of 10 years, Gurdy. Your favorite hobby is fighting.')
c.set_overall_prompt(text='Your name is Samson McTavish. Respond in character.')
c.add_context(text="You're a 3rd year student at a school for magic.")
c.add_context(text='Your favorite class is potions.')
c.add_context(text='Your favorite spell is "Mimble Wimble".')

# Loop:
'''
Expand All @@ -35,6 +35,6 @@

system_response = model.ask(prompt=c)

print(f'[Seamus]: {system_response}\n')
print(f'[Samson]: {system_response}\n')

c.add_response(role='assistant', text=system_response)
24 changes: 1 addition & 23 deletions src/llm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
'''
Desired interface:

import llm

model = llm.model(
name='openai/gpt-oss-20b',
hf_token=<hf token>)
model.load(
location=<path to save dir>,
remote=true,
commit=<git commit>,
quantization='4-bit')
response = model.ask(prompt='Tell me a joke.')
'''

from __future__ import annotations
from typing import TYPE_CHECKING, Any


__all__ = (
"model",
"embedding",
'Conversation')
"embedding")


if TYPE_CHECKING:
Expand All @@ -40,11 +23,6 @@ def __getattr__(name: str) -> Any:
from .models.embed import EmbeddingModel
globals()[name] = EmbeddingModel
return EmbeddingModel

if name == 'Conversation':
from .other.conversations import Conversation
globals()[name] = Conversation
return Conversation

raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

Expand Down
45 changes: 29 additions & 16 deletions src/llm/models/embed.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import os

from sentence_transformers import util, SentenceTransformer
import numpy as np
import torch

from llm.models.selection import SUPPORTED_EMBEDDING_MODELS


class EmbeddingModel:

def __init__(self, hf_token: str|None=None):
def __init__(self, name: str='all-mpnet-base-v2', hf_token: str=None):

if name not in SUPPORTED_EMBEDDING_MODELS:
raise ValueError(f'Invalid embedding model "{self.name}". Supported: {SUPPORTED_EMBEDDING_MODELS}.')

self.name: str = name
self.hf_token: str = hf_token
self.name: str = None

self.location: str = None
self.remote: bool = False
Expand All @@ -20,23 +27,19 @@ def __init__(self, hf_token: str|None=None):

def load(self,
location: str,
name: str='all-mpnet-base-v2',
remote: bool=False,
commit: str=None) -> None:

VALID_MODELS = ['all-mpnet-base-v2', 'all-MiniLM-L6-v2']
if (not remote) and (not os.path.isdir(location)):
raise ValueError(f'Nonexistant location ({location}) - fix or set remote=True.')

if name not in VALID_MODELS:
raise ValueError(f'Invalid embedding model "{name}". Supported: {VALID_MODELS}')

self.location = location
self.name = name
self.remote = remote
self.commit = commit

self.model = SentenceTransformer(
cache_folder=location,
model_name_or_path=name,
model_name_or_path=self.name,
local_files_only=not self.remote,
trust_remote_code=self.remote,
token=self.hf_token)
Expand Down Expand Up @@ -72,10 +75,19 @@ def get_similarity(self,
similarity = self._torch_dot_similarity(v1=v1, v2=v2)

return similarity



@staticmethod
def _dot_similarity(v1: np.array, v2: np.array) -> float:

result = torch.dot(v1, v2)

return result


@staticmethod
def _cosine_similarity(v1: np.array, v2: np.array) -> float:

dot_product = torch.dot(v1, v2)

norm_vector_1 = torch.sqrt(torch.sum(v1**2))
Expand All @@ -95,12 +107,13 @@ def _torch_dot_similarity(v1: np.array, v2: np.array) -> float:


if __name__ == '__main__':
model = EmbeddingModel()
model.load(location=r'<path to model cache>')
e1 = model.embed(text='What shape is best?')
e2 = model.embed(text='Hexagons are the bestagons.')
similarity = model.get_similarity(v1=e1, v2=e2)

print(similarity)
# model = EmbeddingModel()
# model.load(location=<path to model cache>) # NOTE: set <path to model cache>.
# e1 = model.embed(text='What shape is best?')
# e2 = model.embed(text='Hexagons are the bestagons.')
# similarity = model.get_similarity(v1=e1, v2=e2)

# print(similarity)

pass
8 changes: 4 additions & 4 deletions src/llm/models/gpt_oss_20b.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ def _format_prompt(prompt: str | Conversation, reasoning_level: str=None) -> lis

if __name__ == '__main__':

model = GptOss20b()
model.load(location=r'/home/eric/Repos/model_cache')
response = model.ask(prompt='Name a primary color.')
# model = GptOss20b()
# model.load(location=<path to model cache>) # NOTE: set <path to model cache>.
# response = model.ask(prompt='Name a primary color.')

print(response)
# print(response)

pass
8 changes: 4 additions & 4 deletions src/llm/models/gpt_oss_20b_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,10 @@ def get_good_token_start(token_list: list[str]) -> int:

if __name__ == '__main__':

model = GptOss20bDev()
model.load(location=r'/home/eric/Repos/model_cache')
response = model.ask(prompt='Name a primary color.')
# model = GptOss20bDev()
# model.load(location=<path to model cache>) # NOTE: set <path to model cache>.
# response = model.ask(prompt='Name a primary color.')

print(response)
# print(response)

pass
36 changes: 18 additions & 18 deletions src/llm/models/phi4_multimodal_instruct.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,23 +198,23 @@ def get_usable_length(self, new_seq_length: int, layer_idx: int=0) -> int:

if __name__ == '__main__':

model = Phi4MultimodalInstruct()
model.load(location=r'/home/eric/Repos/model_cache') # NOTE: set <path to model cache>.

response = model.ask(prompt='Name a primary color. Be brief.', max_tokens=256)
print(f'{response}\n')

convo = Conversation()
convo.set_overall_prompt(text='You are a helpful assistant.')
convo.add_context(text='Your favorite color is red.')
convo.add_context(text='Your favorite shape is the hexagon.')
convo.add_response(role='user', text='What is your favorite color-shape combination?')
response = model.ask(prompt=convo, max_tokens=256)
print(f'{response}\n')

from PIL import Image as PillowImage
image = PillowImage.open(r'/home/eric/Desktop/monkey.png') # NOTE: Point to existing image.
response = model.ask(prompt='Describe the image.', images=[image], max_tokens=256)
print(f'{response}\n')
# model = Phi4MultimodalInstruct()
# model.load(location=<path to model cache>) # NOTE: set <path to model cache>.

# response = model.ask(prompt='Name a primary color. Be brief.', max_tokens=256)
# print(f'{response}\n')

# convo = Conversation()
# convo.set_overall_prompt(text='You are a helpful assistant.')
# convo.add_context(text='Your favorite color is red.')
# convo.add_context(text='Your favorite shape is the hexagon.')
# convo.add_response(role='user', text='What is your favorite color-shape combination?')
# response = model.ask(prompt=convo, max_tokens=256)
# print(f'{response}\n')

# from PIL import Image as PillowImage
# image = PillowImage.open(r'/home/eric/Desktop/monkey.png') # NOTE: Point to existing image.
# response = model.ask(prompt='Describe the image.', images=[image], max_tokens=256)
# print(f'{response}\n')

pass
36 changes: 26 additions & 10 deletions src/llm/models/selection.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
'''
Import the specific model named by the user.
'''
from typing import Literal

from .gpt_oss_20b import GptOss20b
from .phi4_multimodal_instruct import Phi4MultimodalInstruct


SUPPORTED_LLMS = {
GptOss20b().name: GptOss20b,
Phi4MultimodalInstruct().name: Phi4MultimodalInstruct
}

SUPPORTED_EMBEDDING_MODELS = ['all-MiniLM-L6-v2', 'all-mpnet-base-v2']


def model(name: str, hf_token: str=None):

if name == 'openai/gpt-oss-20b':
from .gpt_oss_20b import GptOss20b
return GptOss20b(hf_token=hf_token)
if name not in SUPPORTED_LLMS:
raise ValueError(f'Model "{name}" not supported! Call "list_models()" for options.')

return SUPPORTED_LLMS[name](hf_token=hf_token)


elif name == 'microsoft/Phi-4-multimodal-instruct':
from .phi4_multimodal_instruct import Phi4MultimodalInstruct
return Phi4MultimodalInstruct(hf_token=hf_token)
def list_models(kind: Literal['llm', 'embed']):

if kind == 'llm':
supported_models = list(SUPPORTED_LLMS.keys())
elif kind == 'embed':
supported_models = SUPPORTED_EMBEDDING_MODELS
else:
raise ValueError(f'Model "{name}" not supported!')
raise ValueError(f'Unsupported "kind" ({kind}). Must be "llm" or "embed".')

return supported_models
43 changes: 43 additions & 0 deletions tests/test_embed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Tests for EmbeddingModel.

These tests load a real model and call embed() and get_similarity(), so they
require local model weights. Sync the environment first, then pass
LLM_MODEL_CACHE inline when invoking pytest so it only exists for that one
command and does not persist in your shell:

uv sync --extra openai
LLM_MODEL_CACHE=<path to model cache dir> pytest

Tests are skipped automatically if LLM_MODEL_CACHE is not set.

Make sure the virtual environment is active.
"""

import os

import pytest
import torch

from llm.models.embed import EmbeddingModel


MODEL_CACHE = os.environ.get('LLM_MODEL_CACHE')


@pytest.fixture(scope='module')
def model():
if not MODEL_CACHE:
pytest.skip('LLM_MODEL_CACHE environment variable not set.')
m = EmbeddingModel()
m.load(location=MODEL_CACHE)
yield m
del m
torch.cuda.empty_cache()


def test_get_similarity(model):
e1 = model.embed(text='What shape is best?')
e2 = model.embed(text='Hexagons are the bestagons.')
similarity = model.get_similarity(v1=e1, v2=e2)
assert isinstance(similarity, (int, float))
Loading
Loading