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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,13 @@ strict = true
members = [
"serapeum-core",
"serapeum-integrations/llms/serapeum-ollama",
"serapeum-integrations/llms/serapeum-llms-huggingface"
]

[tool.uv.sources]
serapeum-core = { workspace = true }
serapeum-ollama = { workspace = true }
serapeum-llms-huggingface = { workspace = true }

#[tool.uv]
#managed = false
Expand Down
36 changes: 18 additions & 18 deletions serapeum-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,37 +46,37 @@ Python 3.11+ is required.
### 1) Build a minimal LLM implementation

```python
from serapeum.core.llm import LLM
from serapeum.core.llms import LLM
from serapeum.core.base.llms.models import CompletionResponse, Metadata
from serapeum.core.prompts import PromptTemplate


class EchoLLM(LLM):
metadata = Metadata.model_construct(is_chat_model=False)
metadata = Metadata.model_construct(is_chat_model=False)

def chat(self, messages, **kwargs):
raise NotImplementedError()
def chat(self, messages, **kwargs):
raise NotImplementedError()

def stream_chat(self, messages, **kwargs):
raise NotImplementedError()
def stream_chat(self, messages, **kwargs):
raise NotImplementedError()

async def achat(self, messages, **kwargs):
raise NotImplementedError()
async def achat(self, messages, **kwargs):
raise NotImplementedError()

async def astream_chat(self, messages, **kwargs):
raise NotImplementedError()
async def astream_chat(self, messages, **kwargs):
raise NotImplementedError()

def complete(self, prompt, formatted=False, **kwargs):
return CompletionResponse(text=prompt, delta=prompt)
def complete(self, prompt, formatted=False, **kwargs):
return CompletionResponse(text=prompt, delta=prompt)

def stream_complete(self, prompt, formatted=False, **kwargs):
raise NotImplementedError()
def stream_complete(self, prompt, formatted=False, **kwargs):
raise NotImplementedError()

async def acomplete(self, prompt, formatted=False, **kwargs):
return CompletionResponse(text=prompt, delta=prompt)
async def acomplete(self, prompt, formatted=False, **kwargs):
return CompletionResponse(text=prompt, delta=prompt)

async def astream_complete(self, prompt, formatted=False, **kwargs):
raise NotImplementedError()
async def astream_complete(self, prompt, formatted=False, **kwargs):
raise NotImplementedError()


llm = EchoLLM()
Expand Down
3 changes: 2 additions & 1 deletion serapeum-core/src/serapeum/core/base/llms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def metadata(self) -> Metadata:
Metadata: LLM metadata containing various information about the LLM.
"""

def convert_chat_messages(self, messages: Sequence[Message]) -> list[Any]:
@staticmethod
def convert_chat_messages(messages: Sequence[Message]) -> list[Any]:
"""Convert chat messages to an LLM specific message format."""
converted_messages = []
for message in messages:
Expand Down
32 changes: 31 additions & 1 deletion serapeum-core/src/serapeum/core/base/llms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@
"""
texts = [b.content for b in self.chunks if isinstance(b, TextChunk)]
result = (
None if not texts else (texts[0] if len(texts) == 1 else "\n".join(texts))

Check warning on line 223 in serapeum-core/src/serapeum/core/base/llms/models.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested conditional expression into an independent statement.

See more on https://sonarcloud.io/project/issues?id=serapeum-org_serapeum&issues=AZ-a036rtfFoT9tNxh9K&open=AZ-a036rtfFoT9tNxh9K&pullRequest=10
)

return result
Expand Down Expand Up @@ -288,7 +288,7 @@
"""Return the number of messages in the list."""
return len(self.messages)

def __getitem__(self, index: int | slice) -> Message | "MessageList":
def __getitem__(self, index: int | slice) -> "Message | MessageList":
"""Retrieve a message or slice of messages."""
if isinstance(index, slice):
return MessageList(messages=self.messages[index])
Expand Down Expand Up @@ -438,6 +438,16 @@
"""Return the textual content of the completion response."""
return self.text

def to_chat_response(self) -> ChatResponse:
"""Convert a chat response to a chat response."""
return ChatResponse(
message=Message(
role=MessageRole.ASSISTANT,
content=self.text,
additional_kwargs=self.additional_kwargs,
),
raw=self.raw,
)

CompletionResponseGen = Generator[CompletionResponse, None, None]
CompletionResponseAsyncGen = AsyncGenerator[CompletionResponse, None]
Expand Down Expand Up @@ -489,3 +499,23 @@
description="The role this specific LLM provider"
"expects for system prompt. E.g. 'SYSTEM' for OpenAI, 'CHATBOT' for Cohere",
)


def stream_completion_response_to_chat_response(
completion_response_gen: CompletionResponseGen,
) -> ChatResponseGen:
"""Convert a stream completion response to a stream chat response."""

def gen() -> ChatResponseGen:
for response in completion_response_gen:
yield ChatResponse(
message=Message(
role=MessageRole.ASSISTANT,
content=response.text,
additional_kwargs=response.additional_kwargs,
),
delta=response.delta,
raw=response.raw,
)

return gen()
12 changes: 12 additions & 0 deletions serapeum-core/src/serapeum/core/base/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from enum import Enum


class PydanticProgramMode(str, Enum):
"""Pydantic program mode."""

DEFAULT = "default"
OPENAI = "openai"
LLM = "llm"
FUNCTION = "function"
GUIDANCE = "guidance"
LM_FORMAT_ENFORCER = "lm-format-enforcer"
2 changes: 1 addition & 1 deletion serapeum-core/src/serapeum/core/configs/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from dataclasses import dataclass
from typing import Optional

from serapeum.core.llm import LLM
from serapeum.core.llms import LLM
from serapeum.core.models import StructuredLLMMode


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
Metadata,
TextChunk,
)
from serapeum.core.llm.base import LLM
from serapeum.core.llms.base import LLM

__all__ = [
"LLM",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Generator,
Protocol,
runtime_checkable,
Sequence
)

from pydantic import BaseModel, Field, WithJsonSchema, field_validator, model_validator
Expand All @@ -26,13 +27,16 @@
Message,
MessageList,
MessageRole,
ChatResponse,
CompletionResponse,
stream_completion_response_to_chat_response
)
from serapeum.core.models import Model, StructuredLLMMode
from serapeum.core.output_parsers.models import BaseParser, TokenAsyncGen, TokenGen
from serapeum.core.prompts import BasePromptTemplate, PromptTemplate

if TYPE_CHECKING:
from serapeum.core.llm.structured_llm import StructuredLLM
from serapeum.core.llms.structured_llm import StructuredLLM


@runtime_checkable
Expand All @@ -42,7 +46,7 @@ class MessagesToPromptType(Protocol):
Examples:
- Join message contents into a newline-separated prompt
```python
>>> from serapeum.core.llm.base import MessagesToPromptType
>>> from serapeum.core.llms.base import MessagesToPromptType
>>> from serapeum.core.base.llms.models import Message, MessageRole, MessageList
>>> def newline_join(message_list):
... return '\n'.join(message.content or "" for message in message_list)
Expand Down Expand Up @@ -122,7 +126,7 @@ class CompletionToPromptType(Protocol):
Examples:
- Check that an identity adapter satisfies the protocol
```python
>>> from serapeum.core.llm.base import CompletionToPromptType
>>> from serapeum.core.llms.base import CompletionToPromptType
>>> def identity(prompt: str) -> str:
... return prompt
...
Expand Down Expand Up @@ -206,7 +210,7 @@ def stream_response_to_tokens(
- Collect deltas produced by a completion stream
```python
>>> from serapeum.core.base.llms.models import CompletionResponse
>>> from serapeum.core.llm.base import stream_response_to_tokens
>>> from serapeum.core.llms.base import stream_response_to_tokens
>>> def responses():
... yield CompletionResponse(text="Hello", delta="Hel")
... yield CompletionResponse(text="Hello", delta="lo")
Expand Down Expand Up @@ -291,7 +295,7 @@ async def astream_response_to_tokens(
```python
>>> import asyncio
>>> from serapeum.core.base.llms.models import CompletionResponse
>>> from serapeum.core.llm.base import astream_response_to_tokens
>>> from serapeum.core.llms.base import astream_response_to_tokens
>>> async def responses():
... yield CompletionResponse(text="Hello", delta="Hel")
... yield CompletionResponse(text="Hello", delta="lo")
Expand Down Expand Up @@ -2295,6 +2299,72 @@ def as_structured_llm(
See Also:
StructuredLLM: Provides structured prediction helpers built atop the base LLM.
"""
from serapeum.core.llm.structured_llm import StructuredLLM
from serapeum.core.llms.structured_llm import StructuredLLM

return StructuredLLM(llm=self, output_cls=output_cls, **kwargs)


class CustomLLM(LLM):
"""
Simple abstract base class for custom LLMs.

Subclasses must implement the `__init__`, `_complete`,
`_stream_complete`, and `metadata` methods.
"""

def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)

def chat(self, messages: Sequence[Message], **kwargs: Any) -> ChatResponse:
assert self.messages_to_prompt is not None

prompt = self.messages_to_prompt(messages)
completion_response = self.complete(prompt, formatted=True, **kwargs)
return completion_response.to_chat_response()

def stream_chat(
self, messages: Sequence[Message], **kwargs: Any
) -> ChatResponseGen:
assert self.messages_to_prompt is not None

prompt = self.messages_to_prompt(messages)
completion_response_gen = self.stream_complete(prompt, formatted=True, **kwargs)
return stream_completion_response_to_chat_response(completion_response_gen)

async def achat(
self,
messages: Sequence[Message],
**kwargs: Any,
) -> ChatResponse:
return self.chat(messages, **kwargs)

async def astream_chat(
self,
messages: Sequence[Message],
**kwargs: Any,
) -> ChatResponseAsyncGen:
async def gen() -> ChatResponseAsyncGen:
for message in self.stream_chat(messages, **kwargs):
yield message

# NOTE: convert generator to async generator
return gen()

async def acomplete(
self, prompt: str, formatted: bool = False, **kwargs: Any
) -> CompletionResponse:
return self.complete(prompt, formatted=formatted, **kwargs)

async def astream_complete(
self, prompt: str, formatted: bool = False, **kwargs: Any
) -> CompletionResponseAsyncGen:
async def gen() -> CompletionResponseAsyncGen:
for message in self.stream_complete(prompt, formatted=formatted, **kwargs):
yield message

# NOTE: convert generator to async generator
return gen()

@classmethod
def class_name(cls) -> str:
return "custom_llm"
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
ChatResponseGen,
Message,
)
from serapeum.core.llm.base import LLM
from serapeum.core.llms.base import LLM
from serapeum.core.tools.models import ToolCallArguments

if TYPE_CHECKING:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
achat_to_completion_decorator,
chat_to_completion_decorator,
)
from serapeum.core.llm.base import LLM
from serapeum.core.llms.base import LLM
from serapeum.core.prompts.base import ChatPromptTemplate


Expand Down Expand Up @@ -52,7 +52,7 @@

def chat(self, messages: Sequence[Message], **kwargs: Any) -> ChatResponse:
"""Chat endpoint for LLM."""
# TODO:

Check warning on line 55 in serapeum-core/src/serapeum/core/llms/structured_llm.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=serapeum-org_serapeum&issues=AZ-a035NtfFoT9tNxh9I&open=AZ-a035NtfFoT9tNxh9I&pullRequest=10

# NOTE: we are wrapping existing messages in a ChatPromptTemplate to
# make this work with our ToolOrchestratingLLM, even though
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pydantic import BaseModel

from serapeum.core.configs.configs import Configs
from serapeum.core.llm.base import LLM
from serapeum.core.llms.base import LLM
from serapeum.core.output_parsers import BaseParser, PydanticParser
from serapeum.core.prompts.base import BasePromptTemplate, PromptTemplate
from serapeum.core.structured_tools.models import BasePydanticLLM
Expand Down
4 changes: 2 additions & 2 deletions serapeum-core/src/serapeum/core/structured_tools/tools_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
from pydantic import BaseModel

from serapeum.core.configs.configs import Configs
from serapeum.core.llm.base import LLM
from serapeum.core.llm.function_calling import FunctionCallingLLM
from serapeum.core.llms.base import LLM
from serapeum.core.llms.function_calling import FunctionCallingLLM
from serapeum.core.prompts.base import BasePromptTemplate, PromptTemplate
from serapeum.core.structured_tools.models import BasePydanticLLM, Model
from serapeum.core.structured_tools.utils import StreamingObjectProcessor
Expand Down
4 changes: 2 additions & 2 deletions serapeum-core/src/serapeum/core/structured_tools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

if TYPE_CHECKING:
from serapeum.core.base.llms.models import ChatResponse
from serapeum.core.llm.base import LLM
from serapeum.core.llm.function_calling import FunctionCallingLLM
from serapeum.core.llms.base import LLM
from serapeum.core.llms.function_calling import FunctionCallingLLM
from serapeum.core.prompts.base import BasePromptTemplate
from serapeum.core.structured_tools.models import BasePydanticLLM
from serapeum.core.tools.models import ToolCallArguments
Expand Down Expand Up @@ -219,7 +219,7 @@
parsed.append(obj)

result = (
parsed if parsed else list(fallback) if fallback else [self._parsing_cls()]

Check warning on line 222 in serapeum-core/src/serapeum/core/structured_tools/utils.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested conditional expression into an independent statement.

See more on https://sonarcloud.io/project/issues?id=serapeum-org_serapeum&issues=AZ-a036-tfFoT9tNxh9L&open=AZ-a036-tfFoT9tNxh9L&pullRequest=10
)

return result
Expand Down
35 changes: 34 additions & 1 deletion serapeum-core/src/serapeum/core/utils/base.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
"""Utilities for resolving binary data from various sources into BytesIO objects."""

import base64
import threading
from contextvars import copy_context
from io import BytesIO
from pathlib import Path
from typing import Optional, Union
from functools import partial
from typing import Optional, Union, Any, Callable
from urllib.parse import urlparse

import requests


def resolve_binary(

Check failure on line 15 in serapeum-core/src/serapeum/core/utils/base.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 33 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=serapeum-org_serapeum&issues=AZ-a036VtfFoT9tNxh9J&open=AZ-a036VtfFoT9tNxh9J&pullRequest=10
raw_bytes: Optional[bytes] = None,
path: Optional[Union[str, Path]] = None,
url: Optional[str] = None,
Expand Down Expand Up @@ -141,3 +144,33 @@
if len(text) <= max_length:
return text
return text[: max_length - 3] + "..."

class Thread(threading.Thread):
"""
A wrapper for threading.Thread that copies the current context and uses the copy to run the target.
"""

def __init__(
self,
group: Optional[Any] = None,
target: Optional[Callable[..., Any]] = None,
name: Optional[str] = None,
args: tuple[Any, ...] = (),
kwargs: Optional[dict[str, Any]] = None,
*,
daemon: Optional[bool] = None,
) -> None:
if target is not None:
args = (
partial(target, *args, **(kwargs if isinstance(kwargs, dict) else {})),
)
else:
args = ()

super().__init__(
group=group,
target=copy_context().run,
name=name,
args=args,
daemon=daemon,
)
Loading
Loading