Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement models for BrainForge service #8

Draft
wants to merge 18 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
637e418
Define models for BrainForge services
NeonDaniel Dec 13, 2024
e02a0b6
Allow empty string `system_prompt` in `LLMPersona` to support "vanill…
NeonDaniel Dec 14, 2024
ef2fbac
Fix fields defined with descriptions as default values
NeonDaniel Dec 17, 2024
a660766
Add `BrainForgeLLM.vllm_spec` convenience property
NeonDaniel Dec 18, 2024
d44fbcb
Start defining HTTP models for brainforge endpoints
NeonDaniel Dec 18, 2024
ad2d42c
Add HTTP models for BrainForge service endpoints
NeonDaniel Dec 18, 2024
48b7146
Add missing `models.api.http` init file
NeonDaniel Dec 18, 2024
966534b
Append user `query` to history in `LLMRequest.to_completion_kwargs`
NeonDaniel Dec 18, 2024
11f194c
Add missing license notice
NeonDaniel Dec 18, 2024
ca32adc
Add docstrings to clarify submodule intended usage
NeonDaniel Dec 18, 2024
9dd2ac2
Update documentation and tests to account for history handling change
NeonDaniel Dec 19, 2024
449dcca
Fix test error
NeonDaniel Dec 19, 2024
55bf516
Allow persona definition with `None` system prompt
NeonDaniel Dec 19, 2024
bf36ffa
Prevent inserting `None` system prompt in completion request history
NeonDaniel Dec 19, 2024
24a170c
Define models for generic completion requests/responses
NeonDaniel Dec 19, 2024
8b83a41
Define models for tokenizer request/response
NeonDaniel Dec 20, 2024
82d8e1b
Update raw endpoint models based on actual usage
NeonDaniel Dec 21, 2024
370e5f3
Address review feedback regarding chat template and inference request…
NeonDaniel Dec 23, 2024
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
31 changes: 31 additions & 0 deletions neon_data_models/models/api/http/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System
# All trademark and other rights reserved by their respective owners
# Copyright 2008-2024 Neongecko.com Inc.
# BSD-3
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from neon_data_models.models.api.http.brainforge import *

"""
This module contains models for interacting via HANA (HTTP).
"""
57 changes: 57 additions & 0 deletions neon_data_models/models/api/http/brainforge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System
# All trademark and other rights reserved by their respective owners
# Copyright 2008-2024 Neongecko.com Inc.
# BSD-3
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from typing import List, Optional
from pydantic import Field

from neon_data_models.models.base import BaseModel
from neon_data_models.models.api.llm import BrainForgeLLM, LLMPersona, LLMRequest


class LLMGetModelsHttpResponse(BaseModel):
models: List[BrainForgeLLM]

Choose a reason for hiding this comment

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

This returns both all models and all personas, making Persona related requests useless

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe the brainforge_get_personas endpoint isn't necessary at all? The only use case I see for it now is if some client wants to get a specific model@revision without parsing all of the available models

Choose a reason for hiding this comment

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

I think that it will never be the case
Because every service wants to request all available info at once and validate requests before been sent

But we can keep it, and deside later



class LLMGetPersonasHttpRequest(BaseModel):
model_id: str = Field(
description="Model ID (<name>@<version>) to get personas for")


class LLMGetPersonasHttpResponse(BaseModel):
personas: List[LLMPersona] = Field(
description="List of personas associated with the requested model.")


class LLMGetInferenceHttpRequest(LLMRequest):
llm_name: str = Field(description="Model name to request")
llm_revision: str = Field(description="Model revision to request")
model: Optional[str] = None


__all__ = [LLMGetModelsHttpResponse.__name__,
LLMGetPersonasHttpRequest.__name__,
LLMGetPersonasHttpResponse.__name__,
LLMGetInferenceHttpRequest.__name__]
33 changes: 25 additions & 8 deletions neon_data_models/models/api/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class LLMPersona(BaseModel):
name: str = Field(description="Unique name for this persona")
description: Optional[str] = Field(
None, description="Human-readable description of this persona")
system_prompt: str = Field(
system_prompt: Optional[str] = Field(
None, description="System prompt associated with this persona. "
"If None, `description` will be used.")
enabled: bool = Field(
Expand All @@ -48,8 +48,7 @@ class LLMPersona(BaseModel):

@model_validator(mode='after')
def validate_request(self):
assert any((self.description, self.system_prompt))
if self.system_prompt is None:
if self.system_prompt is None and self.description:
self.system_prompt = self.description
return self

Expand All @@ -71,7 +70,7 @@ class LLMRequest(BaseModel):
"OpenAI-compatible requests.")
persona: LLMPersona = Field(
description="Requested persona to respond to this message")
model: str = Field(description="Model to request")
model: str = Field(description="Model to request (<name>@<revision>)")
max_tokens: int = Field(
default=512, ge=64, le=2048,
description="Maximum number of tokens to include in the response")
Expand All @@ -95,7 +94,8 @@ class LLMRequest(BaseModel):
"Mutually exclusive with `stream`.")
max_history: int = Field(
default=2, description="Maximum number of user/assistant "
"message pairs to include in history context.")
"message pairs to include in history context. "
"Excludes system prompt and incoming query.")

@model_validator(mode='before')
@classmethod
Expand Down Expand Up @@ -165,8 +165,10 @@ def to_completion_kwargs(self, mq2role: dict = None) -> dict:
history = self.messages[-2*self.max_history:]
for msg in history:
msg["role"] = mq2role.get(msg["role"]) or msg["role"]
history.insert(0, {"role": "system",
"content": self.persona.system_prompt})
if self.persona.system_prompt:
history.insert(0, {"role": "system",
"content": self.persona.system_prompt})
history.append({"role": "user", "content": self.query})
return {"model": self.model,
"messages": history,
"max_tokens": self.max_tokens,
Expand Down Expand Up @@ -197,4 +199,19 @@ def validate_inputs(cls, values):
return values


__all__ = [LLMPersona.__name__, LLMRequest.__name__, LLMResponse.__name__]
class BrainForgeLLM(BaseModel):
name: str = Field(description="LLM Name")
version: str = Field(description="LLM Version")
personas: List[LLMPersona] = Field(
default=[], description="List of personas defined in this model")

@property
def vllm_spec(self):
"""
Model identifier used by vllm (<name>@<version>)
"""
return f"{self.name}@{self.version}"


__all__ = [LLMPersona.__name__, LLMRequest.__name__, LLMResponse.__name__,
BrainForgeLLM.__name__]
4 changes: 4 additions & 0 deletions neon_data_models/models/api/mq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@

from neon_data_models.models.api.mq.llm import *
from neon_data_models.models.api.mq.users import *

"""
This module contains models for interacting via the MQ bus.
"""
105 changes: 105 additions & 0 deletions neon_data_models/models/api/mq/brainforge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System
# All trademark and other rights reserved by their respective owners
# Copyright 2008-2024 Neongecko.com Inc.
# BSD-3
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from typing import List, Optional, Any, Dict, Literal
from pydantic import Field

from neon_data_models.models.base.contexts import MQContext
from neon_data_models.models.api.llm import BrainForgeLLM, LLMRequest, LLMResponse, LLMPersona
from neon_data_models.models.api.http.brainforge import LLMGetModelsHttpResponse, LLMGetPersonasHttpRequest


class LLMGetModels(MQContext):
user_id: str = Field(
description="ID of user to get models for")


class LLMGetModelsResponse(MQContext, LLMGetModelsHttpResponse):
pass


class LLMGetPersonas(LLMGetModels, LLMGetPersonasHttpRequest):
@property
def model_name(self):
return self.model_id.split("@")[0]

@property
def model_version(self):
return self.model_id.split("@", 1)[1]


class LLMGetPersonasResponse(MQContext):
model: Optional[BrainForgeLLM] = Field(
"Full configuration of requested model if model is loaded and access "
"is allowed, else None.")

@property
def personas(self) -> List[LLMPersona]:
"""
Convenience property defined to easily reference the personas requested
"""
return self.model.personas if self.model else []


class LLMGetInference(LLMRequest, MQContext):
user_id: str = Field("ID of user making the request")

def as_llm_request(self):
"""
Get a plain `LLMRequest` object from this `LLMGetInference` object.
"""
return LLMRequest(**self.model_dump())


class LLMGetCompletion(LLMGetModels):
model: str = Field(description="Model to request (<name>@<revision>)")
completion_kwargs: Dict[str, Any] = Field(
description="Dictionary of kwargs to pass to OpenAI completion request")


class LLMGetCompletionResponse(MQContext):
openai_response: dict = Field(
description="OpenAI ChatCompletion model")


class LLMGetTokenizerChatTemplate(LLMGetModels):
model: str = Field(description="Model to request (<name>@<revision>)")
messages: List[Dict[Literal["role", "content"], str]] = Field(
description="List of dict messages in OpenAI format")
tokenize: bool = Field(False)


class LLMGetTokenizerChatTemplateResponse(MQContext):
prompt: str = Field(description="Prompt generated by the tokenizer")


class LLMGetInferenceResponse(LLMResponse, MQContext):
pass


__all__ = [LLMGetModels.__name__, LLMGetModelsResponse.__name__,
LLMGetPersonas.__name__, LLMGetPersonasResponse.__name__,
LLMGetInference.__name__, LLMGetInferenceResponse.__name__]
5 changes: 5 additions & 0 deletions neon_data_models/models/api/node_v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
from neon_data_models.models.base.messagebus import BaseMessage, MessageContext


"""
This module contains models for interacting via the Node socket (WS).
"""


class AudioInputData(BaseModel):
audio_data: str = Field(description="base64-encoded audio")
lang: str = Field(description="BCP-47 language code")
Expand Down
6 changes: 4 additions & 2 deletions tests/models/api/test_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_llm_persona(self):
"A customized bot for something")

with self.assertRaises(ValidationError):
LLMPersona(name="underdefined persona")
LLMPersona(description="underdefined persona")

def test_llm_request(self):
from neon_data_models.models.api.llm import LLMRequest, LLMPersona
Expand All @@ -71,7 +71,9 @@ def test_llm_request(self):
self.assertFalse(valid_request.beam_search)
self.assertEqual(len(valid_request.history), len(test_history))
self.assertEqual(len(valid_request.to_completion_kwargs()['messages']),
2 * valid_request.max_history + 1)
2 * valid_request.max_history + 2)
self.assertEqual(valid_request.to_completion_kwargs()['messages'][-1]['content'],
test_query)

# Valid explicit streaming
streaming_request = LLMRequest(query=test_query, history=test_history,
Expand Down
Loading