diff --git a/neon_data_models/models/api/http/__init__.py b/neon_data_models/models/api/http/__init__.py new file mode 100644 index 0000000..19a7ca2 --- /dev/null +++ b/neon_data_models/models/api/http/__init__.py @@ -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). +""" diff --git a/neon_data_models/models/api/http/brainforge.py b/neon_data_models/models/api/http/brainforge.py new file mode 100644 index 0000000..7f099e3 --- /dev/null +++ b/neon_data_models/models/api/http/brainforge.py @@ -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] + + +class LLMGetPersonasHttpRequest(BaseModel): + model_id: str = Field( + description="Model ID (@) 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__] diff --git a/neon_data_models/models/api/llm.py b/neon_data_models/models/api/llm.py index ae0be8f..6200b18 100644 --- a/neon_data_models/models/api/llm.py +++ b/neon_data_models/models/api/llm.py @@ -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( @@ -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 @@ -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 (@)") max_tokens: int = Field( default=512, ge=64, le=2048, description="Maximum number of tokens to include in the response") @@ -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 @@ -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 is not None: + 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, @@ -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 (@) + """ + return f"{self.name}@{self.version}" + + +__all__ = [LLMPersona.__name__, LLMRequest.__name__, LLMResponse.__name__, + BrainForgeLLM.__name__] diff --git a/neon_data_models/models/api/mq/__init__.py b/neon_data_models/models/api/mq/__init__.py index 9fafcf2..b63f694 100644 --- a/neon_data_models/models/api/mq/__init__.py +++ b/neon_data_models/models/api/mq/__init__.py @@ -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. +""" diff --git a/neon_data_models/models/api/mq/brainforge.py b/neon_data_models/models/api/mq/brainforge.py new file mode 100644 index 0000000..9ff8a71 --- /dev/null +++ b/neon_data_models/models/api/mq/brainforge.py @@ -0,0 +1,112 @@ +# 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, Union +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 (@)") + 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 LLMGetTokenizerChatTemplatedString(LLMGetModels): + model: str = Field(description="Model to request (@)") + messages: List[Dict[Literal["role", "content"], str]] = Field( + description="List of dict messages in OpenAI format") + add_generation_prompt: bool = Field( + description="If true, assistant start tokens will be appended to the " + "formatted output.") + tokenize: bool = Field( + False, + description="If true, a list of token strings is returned, " + "else a single string") + + +class LLMGetTokenizerChatTemplatedStringResponse(MQContext): + prompt: Union[List[str], 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__] diff --git a/neon_data_models/models/api/node_v1/__init__.py b/neon_data_models/models/api/node_v1/__init__.py index 3440595..67a422f 100644 --- a/neon_data_models/models/api/node_v1/__init__.py +++ b/neon_data_models/models/api/node_v1/__init__.py @@ -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") diff --git a/tests/models/api/test_llm.py b/tests/models/api/test_llm.py index 8fe8ecf..cc9fc96 100644 --- a/tests/models/api/test_llm.py +++ b/tests/models/api/test_llm.py @@ -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 @@ -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,