brainforge-inference-utils 0.0.1a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brainforge_inference_utils/__init__.py +82 -0
- brainforge_inference_utils/client.py +117 -0
- brainforge_inference_utils/config.py +105 -0
- brainforge_inference_utils/mcp.py +92 -0
- brainforge_inference_utils/pipeline.py +171 -0
- brainforge_inference_utils/registry.py +115 -0
- brainforge_inference_utils/steps/__init__.py +51 -0
- brainforge_inference_utils/steps/base.py +194 -0
- brainforge_inference_utils/transaction.py +79 -0
- brainforge_inference_utils/version.py +31 -0
- brainforge_inference_utils-0.0.1a1.dist-info/METADATA +154 -0
- brainforge_inference_utils-0.0.1a1.dist-info/RECORD +16 -0
- brainforge_inference_utils-0.0.1a1.dist-info/WHEEL +5 -0
- brainforge_inference_utils-0.0.1a1.dist-info/licenses/LICENSE.md +21 -0
- brainforge_inference_utils-0.0.1a1.dist-info/top_level.txt +1 -0
- brainforge_inference_utils-0.0.1a1.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# Contributors: Daniel McKnight, Richard Leeds, Bohdan Mykhailenko,
|
|
5
|
+
# Dmitry Honcharenko, Mike Gray, Clary Gasper, Charlie Bailey, Ryan Venturi,
|
|
6
|
+
# Roger Wiebe, Pierce Seigne, Sathvika Nadipalli, Guy Daniels, Elon Gasper,
|
|
7
|
+
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
|
|
8
|
+
# BSD-3 License
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from this
|
|
18
|
+
# software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
21
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
22
|
+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
26
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
"""BrainForge inference pipeline utilities."""
|
|
31
|
+
|
|
32
|
+
from neon_data_models.models.api.llm import (
|
|
33
|
+
LLMPersona,
|
|
34
|
+
LLMRequest,
|
|
35
|
+
LLMResponse,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from brainforge_inference_utils.version import __version__
|
|
39
|
+
|
|
40
|
+
from brainforge_inference_utils.client import BrainForgeClient
|
|
41
|
+
from brainforge_inference_utils.config import PipelineConfig, StepConfig
|
|
42
|
+
from brainforge_inference_utils.pipeline import (
|
|
43
|
+
RequestPipeline,
|
|
44
|
+
ResponsePipeline,
|
|
45
|
+
build_pipelines,
|
|
46
|
+
)
|
|
47
|
+
from brainforge_inference_utils.registry import (
|
|
48
|
+
REQUEST_STEP_ENTRY_POINT,
|
|
49
|
+
RESPONSE_STEP_ENTRY_POINT,
|
|
50
|
+
)
|
|
51
|
+
from brainforge_inference_utils.steps.base import (
|
|
52
|
+
RequestStep,
|
|
53
|
+
RequestContextOption,
|
|
54
|
+
ResponseStep,
|
|
55
|
+
Step,
|
|
56
|
+
)
|
|
57
|
+
from brainforge_inference_utils.transaction import UserTransaction
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"__version__",
|
|
61
|
+
"BrainForgeClient",
|
|
62
|
+
# Request/response models (re-exported from neon-data-models)
|
|
63
|
+
"LLMRequest",
|
|
64
|
+
"LLMResponse",
|
|
65
|
+
"LLMPersona",
|
|
66
|
+
# Transaction
|
|
67
|
+
"UserTransaction",
|
|
68
|
+
# Steps
|
|
69
|
+
"Step",
|
|
70
|
+
"RequestStep",
|
|
71
|
+
"ResponseStep",
|
|
72
|
+
"RequestContextOption",
|
|
73
|
+
# Pipelines
|
|
74
|
+
"RequestPipeline",
|
|
75
|
+
"ResponsePipeline",
|
|
76
|
+
"build_pipelines",
|
|
77
|
+
"REQUEST_STEP_ENTRY_POINT",
|
|
78
|
+
"RESPONSE_STEP_ENTRY_POINT",
|
|
79
|
+
# Config
|
|
80
|
+
"PipelineConfig",
|
|
81
|
+
"StepConfig",
|
|
82
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# Contributors: Daniel McKnight, Richard Leeds, Bohdan Mykhailenko,
|
|
5
|
+
# Dmitry Honcharenko, Mike Gray, Clary Gasper, Charlie Bailey, Ryan Venturi,
|
|
6
|
+
# Roger Wiebe, Pierce Seigne, Sathvika Nadipalli, Guy Daniels, Elon Gasper,
|
|
7
|
+
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
|
|
8
|
+
# BSD-3 License
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from this
|
|
18
|
+
# software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
21
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
22
|
+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
26
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
"""Configured runner for the request and response pipelines.
|
|
31
|
+
|
|
32
|
+
`BrainForgeClient` does not call an LLM. The host runs inference between
|
|
33
|
+
`run_request_pipeline` and `run_response_pipeline`, and is responsible for keeping
|
|
34
|
+
the returned `UserTransaction` until the response is ready.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from typing import Dict
|
|
40
|
+
|
|
41
|
+
from neon_data_models.models.api.llm import LLMRequest, LLMResponse
|
|
42
|
+
|
|
43
|
+
from brainforge_inference_utils.config import PipelineConfig
|
|
44
|
+
from brainforge_inference_utils.pipeline import build_pipelines
|
|
45
|
+
from brainforge_inference_utils.steps.base import RequestContextOption
|
|
46
|
+
from brainforge_inference_utils.transaction import UserTransaction
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class BrainForgeClient:
|
|
50
|
+
"""Build pipelines from config and run them around host-owned inference."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, config: PipelineConfig):
|
|
53
|
+
self.config = config
|
|
54
|
+
self.request_pipeline, self.response_pipeline = build_pipelines(config)
|
|
55
|
+
|
|
56
|
+
async def supported_request_context(
|
|
57
|
+
self, timeout: float = 10.0
|
|
58
|
+
) -> Dict[str, RequestContextOption]:
|
|
59
|
+
"""
|
|
60
|
+
Collect `request_context` keys and allowed values from the pipeline.
|
|
61
|
+
|
|
62
|
+
The host uses this to populate UI; it does not call MCP itself.
|
|
63
|
+
Duplicate keys across steps are merged: enumerated values are unioned
|
|
64
|
+
in first-seen order, and if any declaration is unconstrained
|
|
65
|
+
(`values is None`) the merged option is unconstrained.
|
|
66
|
+
"""
|
|
67
|
+
merged: Dict[str, RequestContextOption] = {}
|
|
68
|
+
for step in (*self.request_pipeline.steps, *self.response_pipeline.steps):
|
|
69
|
+
for option in await step.supported_request_context(timeout=timeout):
|
|
70
|
+
_merge_request_context_option(merged, option)
|
|
71
|
+
return merged
|
|
72
|
+
|
|
73
|
+
async def run_request_pipeline(
|
|
74
|
+
self, request: LLMRequest
|
|
75
|
+
) -> UserTransaction:
|
|
76
|
+
"""
|
|
77
|
+
Shape `request` and collect step context on a new transaction.
|
|
78
|
+
|
|
79
|
+
The caller must hold the returned transaction until
|
|
80
|
+
`run_response_pipeline` (or drop it if the response pipeline is unused).
|
|
81
|
+
"""
|
|
82
|
+
transaction = UserTransaction(request=request)
|
|
83
|
+
return await self.request_pipeline.run(transaction)
|
|
84
|
+
|
|
85
|
+
async def run_response_pipeline(
|
|
86
|
+
self, transaction: UserTransaction, response: LLMResponse
|
|
87
|
+
) -> UserTransaction:
|
|
88
|
+
"""
|
|
89
|
+
Attach `response` to `transaction` and run the response pipeline.
|
|
90
|
+
|
|
91
|
+
Request-step context is on `transaction.request_step_context`; response steps
|
|
92
|
+
write to `transaction.response_step_context`. The caller must pass through
|
|
93
|
+
the transaction from `run_request_pipeline`.
|
|
94
|
+
"""
|
|
95
|
+
transaction.response = response
|
|
96
|
+
return await self.response_pipeline.run(transaction)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _merge_request_context_option(
|
|
100
|
+
merged: Dict[str, RequestContextOption],
|
|
101
|
+
option: RequestContextOption,
|
|
102
|
+
) -> None:
|
|
103
|
+
prior = merged.get(option.key)
|
|
104
|
+
if prior is None:
|
|
105
|
+
merged[option.key] = option
|
|
106
|
+
return
|
|
107
|
+
description = prior.description or option.description
|
|
108
|
+
if prior.values is None or option.values is None:
|
|
109
|
+
merged[option.key] = RequestContextOption(
|
|
110
|
+
option.key, None, description)
|
|
111
|
+
return
|
|
112
|
+
values = list(prior.values)
|
|
113
|
+
for value in option.values:
|
|
114
|
+
if value not in values:
|
|
115
|
+
values.append(value)
|
|
116
|
+
merged[option.key] = RequestContextOption(
|
|
117
|
+
option.key, values, description)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# Contributors: Daniel McKnight, Richard Leeds, Bohdan Mykhailenko,
|
|
5
|
+
# Dmitry Honcharenko, Mike Gray, Clary Gasper, Charlie Bailey, Ryan Venturi,
|
|
6
|
+
# Roger Wiebe, Pierce Seigne, Sathvika Nadipalli, Guy Daniels, Elon Gasper,
|
|
7
|
+
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
|
|
8
|
+
# BSD-3 License
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from this
|
|
18
|
+
# software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
21
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
22
|
+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
26
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
"""
|
|
31
|
+
Pipeline configuration schema.
|
|
32
|
+
|
|
33
|
+
Configuration is two explicitly ordered lists of step entries: `request` and
|
|
34
|
+
`response`. Order is authoritative; the pipeline runs steps in exactly the
|
|
35
|
+
order given and does not reorder. Each entry names a step `id`
|
|
36
|
+
(`author.step_name`, the entry point name) and optionally carries
|
|
37
|
+
step-specific `config`.
|
|
38
|
+
|
|
39
|
+
Example (YAML)::
|
|
40
|
+
|
|
41
|
+
pipeline:
|
|
42
|
+
request:
|
|
43
|
+
- id: brainforge.locale
|
|
44
|
+
config:
|
|
45
|
+
mcp_url: https://mcp.neonaiservices2.com/mcp
|
|
46
|
+
default_location: Renton, WA, USA
|
|
47
|
+
timeout: 5.0
|
|
48
|
+
- {id: brainforge.rag, config: {mcp_url: https://mcp.neonaiservices2.com/mcp, top_k: 10}}
|
|
49
|
+
- brainforge.history # last: valid, truncated history
|
|
50
|
+
response:
|
|
51
|
+
- id: brainforge.citations
|
|
52
|
+
config:
|
|
53
|
+
mcp_url: https://mcp.neonaiservices2.com/mcp
|
|
54
|
+
openai_url: https://hana.example/brainforge/openai/chat/completions
|
|
55
|
+
|
|
56
|
+
A host typically loads its own config and hands the `pipeline` section here::
|
|
57
|
+
|
|
58
|
+
pipeline_config = PipelineConfig.model_validate(config["pipeline"])
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
from __future__ import annotations
|
|
62
|
+
|
|
63
|
+
from typing import Any, Dict, List
|
|
64
|
+
|
|
65
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class StepConfig(BaseModel):
|
|
69
|
+
"""
|
|
70
|
+
A single step entry within a pipeline.
|
|
71
|
+
|
|
72
|
+
:param id: Step id to resolve and instantiate. This is the entry point
|
|
73
|
+
name, `author.step_name`.
|
|
74
|
+
:param enabled: When false, the entry is skipped at build time (a
|
|
75
|
+
convenient way to turn a step off without deleting its config).
|
|
76
|
+
:param config: Step-specific configuration passed to the step constructor.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(extra="forbid")
|
|
80
|
+
|
|
81
|
+
id: str
|
|
82
|
+
enabled: bool = True
|
|
83
|
+
config: Dict[str, Any] = Field(default_factory=dict)
|
|
84
|
+
|
|
85
|
+
@model_validator(mode="before")
|
|
86
|
+
@classmethod
|
|
87
|
+
def _coerce_shorthand(cls, data: Any) -> Any:
|
|
88
|
+
"""Allow a bare string entry as shorthand for `{id: <string>}`."""
|
|
89
|
+
if isinstance(data, str):
|
|
90
|
+
return {"id": data}
|
|
91
|
+
return data
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PipelineConfig(BaseModel):
|
|
95
|
+
"""
|
|
96
|
+
Ordered step lists for the request and response pipelines.
|
|
97
|
+
|
|
98
|
+
:param request: Ordered `RequestStep` entries, run before generation.
|
|
99
|
+
:param response: Ordered `ResponseStep` entries, run after generation.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
model_config = ConfigDict(extra="forbid")
|
|
103
|
+
|
|
104
|
+
request: List[StepConfig] = Field(default_factory=list)
|
|
105
|
+
response: List[StepConfig] = Field(default_factory=list)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# Contributors: Daniel McKnight, Richard Leeds, Bohdan Mykhailenko,
|
|
5
|
+
# Dmitry Honcharenko, Mike Gray, Clary Gasper, Charlie Bailey, Ryan Venturi,
|
|
6
|
+
# Roger Wiebe, Pierce Seigne, Sathvika Nadipalli, Guy Daniels, Elon Gasper,
|
|
7
|
+
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
|
|
8
|
+
# BSD-3 License
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from this
|
|
18
|
+
# software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
21
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
22
|
+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
26
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
"""Generic MCP client helpers shared by pipeline steps."""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
from contextlib import asynccontextmanager
|
|
36
|
+
from typing import Any, AsyncIterator
|
|
37
|
+
|
|
38
|
+
from fastmcp import Client as MCPClient
|
|
39
|
+
from fastmcp.client.transports import StreamableHttpTransport
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@asynccontextmanager
|
|
43
|
+
async def mcp_client(mcp_url: str) -> AsyncIterator[MCPClient]:
|
|
44
|
+
"""
|
|
45
|
+
Open a short-lived MCP client session for one or more tool calls.
|
|
46
|
+
|
|
47
|
+
:param mcp_url: MCP server URL from step configuration.
|
|
48
|
+
"""
|
|
49
|
+
transport = StreamableHttpTransport(mcp_url)
|
|
50
|
+
async with MCPClient(transport) as client:
|
|
51
|
+
yield client
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def mcp_tool_inner_payload(result: Any) -> dict:
|
|
55
|
+
"""
|
|
56
|
+
Server tool payload from tools/call structuredContent envelope.
|
|
57
|
+
|
|
58
|
+
Newer mcp CallToolResult stores structuredContent as a model field; older
|
|
59
|
+
stacks put it only in model_extra. Fall back to JSON in text content.
|
|
60
|
+
"""
|
|
61
|
+
envelope = getattr(result, "structuredContent", None)
|
|
62
|
+
if not isinstance(envelope, dict):
|
|
63
|
+
envelope = (getattr(result, "model_extra", None) or {}).get(
|
|
64
|
+
"structuredContent") or {}
|
|
65
|
+
inner = envelope.get("result") or {}
|
|
66
|
+
payload = inner.get("result")
|
|
67
|
+
if isinstance(payload, dict):
|
|
68
|
+
return payload
|
|
69
|
+
content = getattr(result, "content", None) or []
|
|
70
|
+
if not content:
|
|
71
|
+
return {}
|
|
72
|
+
try:
|
|
73
|
+
parsed = json.loads(content[0].text)
|
|
74
|
+
except (json.JSONDecodeError, TypeError, AttributeError, IndexError):
|
|
75
|
+
return {}
|
|
76
|
+
inner = parsed.get("result") or {}
|
|
77
|
+
payload = inner.get("result")
|
|
78
|
+
return payload if isinstance(payload, dict) else {}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def mcp_tool_content_parts(tool_result: Any) -> list:
|
|
82
|
+
"""
|
|
83
|
+
Normalize tools/call handling across fastmcp and raw protocol objects.
|
|
84
|
+
"""
|
|
85
|
+
if isinstance(tool_result, list):
|
|
86
|
+
return tool_result
|
|
87
|
+
content = getattr(tool_result, "content", None)
|
|
88
|
+
if isinstance(content, list):
|
|
89
|
+
return content
|
|
90
|
+
raise TypeError(
|
|
91
|
+
f"Unexpected MCP tool result {type(tool_result)!r}; "
|
|
92
|
+
"expected list or .content list")
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# Contributors: Daniel McKnight, Richard Leeds, Bohdan Mykhailenko,
|
|
5
|
+
# Dmitry Honcharenko, Mike Gray, Clary Gasper, Charlie Bailey, Ryan Venturi,
|
|
6
|
+
# Roger Wiebe, Pierce Seigne, Sathvika Nadipalli, Guy Daniels, Elon Gasper,
|
|
7
|
+
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
|
|
8
|
+
# BSD-3 License
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from this
|
|
18
|
+
# software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
21
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
22
|
+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
26
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
"""
|
|
31
|
+
The request and response pipelines.
|
|
32
|
+
|
|
33
|
+
- `RequestPipeline` runs ordered `RequestStep`s over a `UserTransaction` to shape
|
|
34
|
+
its request before generation.
|
|
35
|
+
- `ResponsePipeline` runs ordered `ResponseStep`s over a `UserTransaction` to
|
|
36
|
+
annotate or rewrite its response after generation.
|
|
37
|
+
|
|
38
|
+
Generation itself is out of scope here: a caller runs the request pipeline,
|
|
39
|
+
performs generation separately to populate `transaction.response`, then runs the
|
|
40
|
+
response pipeline. Steps that are inapplicable to a given transaction are
|
|
41
|
+
skipped (with a logged reason) rather than failing the pipeline.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import logging
|
|
47
|
+
from typing import List, Mapping, Sequence, Tuple, Type
|
|
48
|
+
|
|
49
|
+
from brainforge_inference_utils.config import PipelineConfig
|
|
50
|
+
from brainforge_inference_utils.registry import (
|
|
51
|
+
REQUEST_STEP_ENTRY_POINT,
|
|
52
|
+
RESPONSE_STEP_ENTRY_POINT,
|
|
53
|
+
load_entry_point_steps,
|
|
54
|
+
)
|
|
55
|
+
from brainforge_inference_utils.steps.base import (
|
|
56
|
+
RequestStep,
|
|
57
|
+
ResponseStep,
|
|
58
|
+
Step,
|
|
59
|
+
)
|
|
60
|
+
from brainforge_inference_utils.transaction import UserTransaction
|
|
61
|
+
|
|
62
|
+
LOG = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RequestPipeline:
|
|
66
|
+
"""
|
|
67
|
+
Runs `RequestStep`s in order over a transaction's request.
|
|
68
|
+
|
|
69
|
+
:param steps: Ordered request steps.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, steps: Sequence[RequestStep]):
|
|
73
|
+
self.steps: List[RequestStep] = list(steps)
|
|
74
|
+
|
|
75
|
+
async def run(self, transaction: UserTransaction) -> UserTransaction:
|
|
76
|
+
"""
|
|
77
|
+
Run every applicable request step, mutating the transaction in place.
|
|
78
|
+
|
|
79
|
+
:param transaction: The transaction whose request to shape.
|
|
80
|
+
:returns: The same `transaction`.
|
|
81
|
+
"""
|
|
82
|
+
for step in self.steps:
|
|
83
|
+
reason = step.applicable(transaction)
|
|
84
|
+
if reason is not None:
|
|
85
|
+
LOG.debug("Skipping request step %r: %s", step.id, reason)
|
|
86
|
+
continue
|
|
87
|
+
await step.process(transaction)
|
|
88
|
+
return transaction
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ResponsePipeline:
|
|
92
|
+
"""
|
|
93
|
+
Runs `ResponseStep`s in order over a transaction's response.
|
|
94
|
+
|
|
95
|
+
:param steps: Ordered response steps.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, steps: Sequence[ResponseStep]):
|
|
99
|
+
self.steps: List[ResponseStep] = list(steps)
|
|
100
|
+
|
|
101
|
+
async def run(self, transaction: UserTransaction) -> UserTransaction:
|
|
102
|
+
"""
|
|
103
|
+
Run every applicable response step, mutating the transaction in place.
|
|
104
|
+
|
|
105
|
+
:param transaction: The transaction whose response to shape.
|
|
106
|
+
:returns: The same `transaction`.
|
|
107
|
+
"""
|
|
108
|
+
for step in self.steps:
|
|
109
|
+
reason = step.applicable(transaction)
|
|
110
|
+
if reason is not None:
|
|
111
|
+
LOG.debug("Skipping response step %r: %s", step.id, reason)
|
|
112
|
+
continue
|
|
113
|
+
await step.process(transaction)
|
|
114
|
+
return transaction
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _resolve_step(stage_id: str,
|
|
118
|
+
steps: Mapping[str, Type[Step]]) -> Type[Step]:
|
|
119
|
+
try:
|
|
120
|
+
return steps[stage_id]
|
|
121
|
+
except KeyError:
|
|
122
|
+
raise KeyError(
|
|
123
|
+
f"No step registered with id {stage_id!r}. "
|
|
124
|
+
f"Known steps: {sorted(steps)}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_pipelines(
|
|
128
|
+
config: PipelineConfig,
|
|
129
|
+
) -> Tuple[RequestPipeline, ResponsePipeline]:
|
|
130
|
+
"""
|
|
131
|
+
Instantiate both pipelines from configuration.
|
|
132
|
+
|
|
133
|
+
Step classes come from the entry point group for that pipeline
|
|
134
|
+
(`brainforge.inference.request_steps` or
|
|
135
|
+
`brainforge.inference.response_steps`). A response step placed in the
|
|
136
|
+
request list (or vice versa) is absent from that group and fails at
|
|
137
|
+
build time.
|
|
138
|
+
|
|
139
|
+
:param config: Parsed pipeline configuration.
|
|
140
|
+
:returns: `(request_pipeline, response_pipeline)`.
|
|
141
|
+
:raises KeyError: If a configured step id is not registered in that
|
|
142
|
+
pipeline's entry point group.
|
|
143
|
+
:raises TypeError: If an installed entry point is not the step type for
|
|
144
|
+
its group.
|
|
145
|
+
"""
|
|
146
|
+
request_step_classes = load_entry_point_steps(
|
|
147
|
+
REQUEST_STEP_ENTRY_POINT, RequestStep)
|
|
148
|
+
response_step_classes = load_entry_point_steps(
|
|
149
|
+
RESPONSE_STEP_ENTRY_POINT, ResponseStep)
|
|
150
|
+
|
|
151
|
+
request_steps: List[RequestStep] = []
|
|
152
|
+
for entry in config.request:
|
|
153
|
+
if not entry.enabled:
|
|
154
|
+
LOG.info("Request step %r disabled by config", entry.id)
|
|
155
|
+
continue
|
|
156
|
+
cls = _resolve_step(entry.id, request_step_classes)
|
|
157
|
+
request_steps.append(cls(entry.config))
|
|
158
|
+
|
|
159
|
+
response_steps: List[ResponseStep] = []
|
|
160
|
+
for entry in config.response:
|
|
161
|
+
if not entry.enabled:
|
|
162
|
+
LOG.info("Response step %r disabled by config", entry.id)
|
|
163
|
+
continue
|
|
164
|
+
cls = _resolve_step(entry.id, response_step_classes)
|
|
165
|
+
response_steps.append(cls(entry.config))
|
|
166
|
+
|
|
167
|
+
LOG.info("Built request pipeline: %s",
|
|
168
|
+
[s.id for s in request_steps] or "(empty)")
|
|
169
|
+
LOG.info("Built response pipeline: %s",
|
|
170
|
+
[s.id for s in response_steps] or "(empty)")
|
|
171
|
+
return RequestPipeline(request_steps), ResponsePipeline(response_steps)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# Contributors: Daniel McKnight, Richard Leeds, Bohdan Mykhailenko,
|
|
5
|
+
# Dmitry Honcharenko, Mike Gray, Clary Gasper, Charlie Bailey, Ryan Venturi,
|
|
6
|
+
# Roger Wiebe, Pierce Seigne, Sathvika Nadipalli, Guy Daniels, Elon Gasper,
|
|
7
|
+
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
|
|
8
|
+
# BSD-3 License
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from this
|
|
18
|
+
# software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
21
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
22
|
+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
26
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
"""
|
|
31
|
+
Discovery of plugin steps via setuptools entry points.
|
|
32
|
+
|
|
33
|
+
Request and response steps are registered in separate groups, so the same
|
|
34
|
+
`author.step_name` id can exist on both sides. Every step class comes from
|
|
35
|
+
the entry point group for its pipeline.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import logging
|
|
41
|
+
import re
|
|
42
|
+
from importlib.metadata import entry_points
|
|
43
|
+
from typing import Dict, Type
|
|
44
|
+
|
|
45
|
+
from brainforge_inference_utils.steps.base import Step
|
|
46
|
+
|
|
47
|
+
LOG = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
REQUEST_STEP_ENTRY_POINT = "brainforge.inference.request_steps"
|
|
50
|
+
RESPONSE_STEP_ENTRY_POINT = "brainforge.inference.response_steps"
|
|
51
|
+
|
|
52
|
+
# author.step_name — letters, digits, hyphens, and underscores in each part.
|
|
53
|
+
STEP_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _distribution_name(entry_point) -> str:
|
|
57
|
+
dist = getattr(entry_point, "dist", None)
|
|
58
|
+
return getattr(dist, "name", None) or "unknown"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_entry_point_steps(
|
|
62
|
+
group: str,
|
|
63
|
+
expected_type: Type[Step],
|
|
64
|
+
) -> Dict[str, Type[Step]]:
|
|
65
|
+
"""
|
|
66
|
+
Load step classes advertised under `group`.
|
|
67
|
+
|
|
68
|
+
Each entry point name must be `author.step_name` and must equal the
|
|
69
|
+
loaded class's `id`. Each part may contain letters, digits, hyphens, and
|
|
70
|
+
underscores. Names must be unique within the group. The same name may
|
|
71
|
+
still be registered under the other pipeline's group.
|
|
72
|
+
|
|
73
|
+
:param group: Entry point group to read.
|
|
74
|
+
:param expected_type: `RequestStep` or `ResponseStep`. Loaded classes must
|
|
75
|
+
subclass it.
|
|
76
|
+
:returns: Entry point name -> step class.
|
|
77
|
+
:raises ValueError: If a name is malformed, duplicated, or differs from
|
|
78
|
+
the class `id`.
|
|
79
|
+
:raises TypeError: If a loaded object is not a subclass of `expected_type`.
|
|
80
|
+
"""
|
|
81
|
+
seen: Dict[str, str] = {}
|
|
82
|
+
loaded: Dict[str, Type[Step]] = {}
|
|
83
|
+
for entry_point in entry_points(group=group):
|
|
84
|
+
dist_name = _distribution_name(entry_point)
|
|
85
|
+
if not STEP_ID_PATTERN.fullmatch(entry_point.name):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Entry point {entry_point.name!r} in {group} from "
|
|
88
|
+
f"{dist_name} must use the form author.step_name")
|
|
89
|
+
prior = seen.get(entry_point.name)
|
|
90
|
+
if prior is not None:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Multiple packages register {group} step "
|
|
93
|
+
f"{entry_point.name!r}: {prior} and {dist_name}")
|
|
94
|
+
seen[entry_point.name] = dist_name
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
step_cls = entry_point.load()
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
raise ImportError(
|
|
100
|
+
f"Failed to load step {entry_point.name!r} from {dist_name}"
|
|
101
|
+
) from exc
|
|
102
|
+
if not (isinstance(step_cls, type) and issubclass(step_cls, expected_type)):
|
|
103
|
+
raise TypeError(
|
|
104
|
+
f"Entry point {entry_point.name!r} in {group} from {dist_name} "
|
|
105
|
+
f"loaded {getattr(step_cls, '__name__', step_cls)!r}, which is "
|
|
106
|
+
f"not a {expected_type.__name__}")
|
|
107
|
+
if step_cls.id != entry_point.name:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Step {step_cls.__name__} from {dist_name} has id "
|
|
110
|
+
f"{step_cls.id!r}, which does not match entry point "
|
|
111
|
+
f"{entry_point.name!r}")
|
|
112
|
+
loaded[entry_point.name] = step_cls
|
|
113
|
+
LOG.debug("Loaded %s step %r from %s",
|
|
114
|
+
expected_type.__name__, entry_point.name, dist_name)
|
|
115
|
+
return loaded
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# Contributors: Daniel McKnight, Richard Leeds, Bohdan Mykhailenko,
|
|
5
|
+
# Dmitry Honcharenko, Mike Gray, Clary Gasper, Charlie Bailey, Ryan Venturi,
|
|
6
|
+
# Roger Wiebe, Pierce Seigne, Sathvika Nadipalli, Guy Daniels, Elon Gasper,
|
|
7
|
+
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
|
|
8
|
+
# BSD-3 License
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from this
|
|
18
|
+
# software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
21
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
22
|
+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
26
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
"""
|
|
31
|
+
Step base classes.
|
|
32
|
+
|
|
33
|
+
Concrete steps ship in separate plugins and are discovered from entry points.
|
|
34
|
+
Step ids use `author.step_name`.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from brainforge_inference_utils.steps.base import (
|
|
40
|
+
RequestStep,
|
|
41
|
+
RequestContextOption,
|
|
42
|
+
ResponseStep,
|
|
43
|
+
Step,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"Step",
|
|
48
|
+
"RequestStep",
|
|
49
|
+
"ResponseStep",
|
|
50
|
+
"RequestContextOption",
|
|
51
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# Contributors: Daniel McKnight, Richard Leeds, Bohdan Mykhailenko,
|
|
5
|
+
# Dmitry Honcharenko, Mike Gray, Clary Gasper, Charlie Bailey, Ryan Venturi,
|
|
6
|
+
# Roger Wiebe, Pierce Seigne, Sathvika Nadipalli, Guy Daniels, Elon Gasper,
|
|
7
|
+
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
|
|
8
|
+
# BSD-3 License
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from this
|
|
18
|
+
# software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
21
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
22
|
+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
26
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
"""
|
|
31
|
+
Step base classes.
|
|
32
|
+
|
|
33
|
+
Two step types plug into the two pipelines:
|
|
34
|
+
|
|
35
|
+
- `RequestStep` runs before generation. It shapes the request on the
|
|
36
|
+
`UserTransaction` (RAG/MCP context, ambient context, request history
|
|
37
|
+
validation, ...), mutating it in place.
|
|
38
|
+
- `ResponseStep` runs after generation. It shapes the response on the
|
|
39
|
+
`UserTransaction` (citations, sanitizing, guardrails, ...), mutating it
|
|
40
|
+
in place.
|
|
41
|
+
|
|
42
|
+
Both share two conventions:
|
|
43
|
+
|
|
44
|
+
1. **Defensive, not dependent.** Steps declare no relationships to one another.
|
|
45
|
+
A step that requires something it does not find reports itself inapplicable
|
|
46
|
+
from `applicable()` and is skipped (with a logged reason) rather than failing
|
|
47
|
+
the pipeline. This keeps configuration order the only ordering contract and
|
|
48
|
+
lets steps be added, removed, or substituted freely.
|
|
49
|
+
|
|
50
|
+
2. **Shared object, separate namespaces.** Data that must pass between steps
|
|
51
|
+
travels on the `UserTransaction`, not through a step-to-step interface.
|
|
52
|
+
Request steps use `transaction.request_step_context`; response steps use
|
|
53
|
+
`transaction.response_step_context`. Step ids are unique only within one side.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
from abc import ABC, abstractmethod
|
|
59
|
+
from dataclasses import dataclass
|
|
60
|
+
from typing import Any, ClassVar, List, Optional, Sequence
|
|
61
|
+
|
|
62
|
+
from brainforge_inference_utils.transaction import UserTransaction
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class RequestContextOption:
|
|
67
|
+
"""
|
|
68
|
+
One `UserTransaction.request_context` key a step reads.
|
|
69
|
+
|
|
70
|
+
Hosts collect these from the built pipeline to present the keys and, when
|
|
71
|
+
known, the values a client may send. `values is None` means the key is
|
|
72
|
+
supported but unconstrained (any value of the implied type). An empty
|
|
73
|
+
`values` list means the key is supported but no choices are available
|
|
74
|
+
right now (for example an empty live catalog).
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
key: str
|
|
78
|
+
values: Optional[Sequence[Any]] = None
|
|
79
|
+
description: str = ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Step(ABC):
|
|
83
|
+
"""
|
|
84
|
+
Common base for all pipeline steps.
|
|
85
|
+
|
|
86
|
+
A step is constructed once, at pipeline build time, with its resolved
|
|
87
|
+
configuration. The same instance is reused across transactions, so steps
|
|
88
|
+
should keep per-request state on the `UserTransaction`, not on `self`.
|
|
89
|
+
|
|
90
|
+
:cvar id: Unique step identifier used in configuration. Subclasses must set
|
|
91
|
+
this to a stable, non-empty string. Plugin steps use `author.step_name`
|
|
92
|
+
and that string must match the entry point name.
|
|
93
|
+
:cvar request_context_options: Static `request_context` keys this step
|
|
94
|
+
reads. Override `supported_request_context` when allowed values must be
|
|
95
|
+
fetched live (for example a provider catalog).
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
id: ClassVar[str] = ""
|
|
99
|
+
request_context_options: ClassVar[Sequence[RequestContextOption]] = ()
|
|
100
|
+
|
|
101
|
+
def __init__(self, config: Optional[dict] = None):
|
|
102
|
+
"""
|
|
103
|
+
:param config: Step-specific configuration from the pipeline config
|
|
104
|
+
entry's `config` mapping. Stored as `self.config`.
|
|
105
|
+
"""
|
|
106
|
+
self.config: dict = dict(config or {})
|
|
107
|
+
|
|
108
|
+
def __repr__(self) -> str:
|
|
109
|
+
return f"<{type(self).__name__} id={self.id!r}>"
|
|
110
|
+
|
|
111
|
+
async def supported_request_context(
|
|
112
|
+
self, timeout: float = 10.0
|
|
113
|
+
) -> List[RequestContextOption]:
|
|
114
|
+
"""
|
|
115
|
+
Return the `request_context` keys this step reads, with allowed values.
|
|
116
|
+
|
|
117
|
+
The default copies `request_context_options`. Override when values must
|
|
118
|
+
be fetched live (for example a provider catalog). `timeout` is unused
|
|
119
|
+
for static declarations.
|
|
120
|
+
|
|
121
|
+
:param timeout: Seconds to wait for a live catalog, when overridden.
|
|
122
|
+
"""
|
|
123
|
+
return list(self.request_context_options)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class RequestStep(Step, ABC):
|
|
127
|
+
"""
|
|
128
|
+
A step that shapes the request before generation.
|
|
129
|
+
|
|
130
|
+
Implementations mutate `transaction.request` in place (selecting a persona,
|
|
131
|
+
trimming or compacting history, injecting retrieved context, adjusting
|
|
132
|
+
generation params, ...).
|
|
133
|
+
|
|
134
|
+
Steps that read `transaction.request_context` should declare those keys on
|
|
135
|
+
`request_context_options` (static choices) or override
|
|
136
|
+
`supported_request_context` (live catalogs). Hosts collect the result via
|
|
137
|
+
`BrainForgeClient.supported_request_context`.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def applicable(self, transaction: UserTransaction) -> Optional[str]:
|
|
141
|
+
"""
|
|
142
|
+
Decide whether this step should run for the given transaction.
|
|
143
|
+
|
|
144
|
+
:param transaction: The current transaction.
|
|
145
|
+
:returns: `None` to run the step, or a short human-readable reason to
|
|
146
|
+
skip it (logged). The default runs unconditionally; override to add
|
|
147
|
+
per-request gating.
|
|
148
|
+
"""
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
@abstractmethod
|
|
152
|
+
async def process(self, transaction: UserTransaction) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Apply this step's effect to the request.
|
|
155
|
+
|
|
156
|
+
Only called when `applicable(transaction)` returned `None`.
|
|
157
|
+
Implementations mutate `transaction` in place and return nothing.
|
|
158
|
+
|
|
159
|
+
:param transaction: The transaction whose `request` to shape.
|
|
160
|
+
"""
|
|
161
|
+
raise NotImplementedError
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ResponseStep(Step, ABC):
|
|
165
|
+
"""
|
|
166
|
+
A step that shapes the generated response.
|
|
167
|
+
|
|
168
|
+
Implementations mutate `transaction.response` in place (annotating or
|
|
169
|
+
rewriting it). They return nothing.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def applicable(self, transaction: UserTransaction) -> Optional[str]:
|
|
173
|
+
"""
|
|
174
|
+
Decide whether this step should run for the given transaction.
|
|
175
|
+
|
|
176
|
+
:param transaction: The current transaction (with `response` populated).
|
|
177
|
+
:returns: `None` to run the step, or a short human-readable reason to
|
|
178
|
+
skip it (logged). The default runs unconditionally; override to add
|
|
179
|
+
per-request gating (e.g. skip when there is nothing to annotate).
|
|
180
|
+
"""
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
@abstractmethod
|
|
184
|
+
async def process(self, transaction: UserTransaction) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Apply this step's effect to the response.
|
|
187
|
+
|
|
188
|
+
Only called when `applicable(transaction)` returned `None`.
|
|
189
|
+
Implementations mutate `transaction.response` in place and return
|
|
190
|
+
nothing.
|
|
191
|
+
|
|
192
|
+
:param transaction: The transaction whose `response` to shape.
|
|
193
|
+
"""
|
|
194
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# Contributors: Daniel McKnight, Richard Leeds, Bohdan Mykhailenko,
|
|
5
|
+
# Dmitry Honcharenko, Mike Gray, Clary Gasper, Charlie Bailey, Ryan Venturi,
|
|
6
|
+
# Roger Wiebe, Pierce Seigne, Sathvika Nadipalli, Guy Daniels, Elon Gasper,
|
|
7
|
+
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
|
|
8
|
+
# BSD-3 License
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from this
|
|
18
|
+
# software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
21
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
22
|
+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
26
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
"""
|
|
31
|
+
The object a pipeline operates on.
|
|
32
|
+
|
|
33
|
+
`UserTransaction` represents one user request/response cycle. Request steps shape
|
|
34
|
+
its `request` and write `request_step_context`; generation (performed elsewhere)
|
|
35
|
+
populates its `response`; response steps annotate that `response` and write
|
|
36
|
+
`response_step_context`.
|
|
37
|
+
|
|
38
|
+
The request and response are the existing `neon-data-models` models used across
|
|
39
|
+
the BrainForge stack (HANA, `brainforge-llm-service`), so this package does not
|
|
40
|
+
redefine their shape.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
from typing import Any, Dict, Optional
|
|
46
|
+
|
|
47
|
+
from pydantic import BaseModel, Field
|
|
48
|
+
|
|
49
|
+
from neon_data_models.models.api.llm import LLMRequest, LLMResponse
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class UserTransaction(BaseModel):
|
|
53
|
+
"""
|
|
54
|
+
One user request/response cycle as it flows through the pipelines.
|
|
55
|
+
|
|
56
|
+
:param request: The request being shaped by the request pipeline.
|
|
57
|
+
:param response: The generated response, populated after generation and
|
|
58
|
+
shaped by the response pipeline. `None` until generation completes.
|
|
59
|
+
:param request_step_context: Transient data written by request steps, keyed by
|
|
60
|
+
request step id.
|
|
61
|
+
:param response_step_context: Transient data written by response steps, keyed by
|
|
62
|
+
response step id.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
request: LLMRequest = Field(
|
|
66
|
+
description="The LLM request shaped by the request pipeline")
|
|
67
|
+
response: Optional[LLMResponse] = Field(
|
|
68
|
+
default=None,
|
|
69
|
+
description="The generated response shaped by the response pipeline; "
|
|
70
|
+
"None until generation completes")
|
|
71
|
+
request_context: Dict[str, Any] = Field(
|
|
72
|
+
default_factory=dict,
|
|
73
|
+
description="Client-provided context read by request steps")
|
|
74
|
+
request_step_context: Dict[str, Any] = Field(
|
|
75
|
+
default_factory=dict,
|
|
76
|
+
description="Request-step context keyed by request step id")
|
|
77
|
+
response_step_context: Dict[str, Any] = Field(
|
|
78
|
+
default_factory=dict,
|
|
79
|
+
description="Response-step context keyed by response step id")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# Contributors: Daniel McKnight, Richard Leeds, Bohdan Mykhailenko,
|
|
5
|
+
# Dmitry Honcharenko, Mike Gray, Clary Gasper, Charlie Bailey, Ryan Venturi,
|
|
6
|
+
# Roger Wiebe, Pierce Seigne, Sathvika Nadipalli, Guy Daniels, Elon Gasper,
|
|
7
|
+
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
|
|
8
|
+
# BSD-3 License
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from this
|
|
18
|
+
# software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
21
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
22
|
+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
|
26
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
__version__ = "0.0.1a1"
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brainforge-inference-utils
|
|
3
|
+
Version: 0.0.1a1
|
|
4
|
+
Summary: Pluggable request/response pipelines for BrainForge LLM inference
|
|
5
|
+
Home-page: https://github.com/NeonGeckoCom/brainforge-inference-utils
|
|
6
|
+
Author: NeonGecko
|
|
7
|
+
Author-email: developers@neon.ai
|
|
8
|
+
License: AGPL-3.0-only
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE.md
|
|
13
|
+
Requires-Dist: pydantic~=2.0
|
|
14
|
+
Requires-Dist: neon-data-models>=0.0.3a8,~=0.0.2
|
|
15
|
+
Requires-Dist: fastmcp~=2.3
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest~=8.0; extra == "test"
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: provides-extra
|
|
28
|
+
Dynamic: requires-dist
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# BrainForge Inference Utilities
|
|
32
|
+
|
|
33
|
+
Pluggable pipelines for shaping BrainForge LLM requests and responses.
|
|
34
|
+
|
|
35
|
+
This package defines request/response orchestration that attaches to an
|
|
36
|
+
existing inference workflow. Concrete behavior (ambient context, RAG/MCP
|
|
37
|
+
injection, request history validation, citations, response sanitizing,
|
|
38
|
+
safety guardrails, ...) can be implemented as **steps** that plug into one of
|
|
39
|
+
two pipelines:
|
|
40
|
+
|
|
41
|
+
- **Request pipeline** — runs before generation and shapes the request:
|
|
42
|
+
injecting informational context, RAG retrieval for host-selected
|
|
43
|
+
providers, validating and truncating history, etc.
|
|
44
|
+
- **Response pipeline** — runs after generation and shapes the response:
|
|
45
|
+
citation rewrite, sanitization, guardrails, etc.
|
|
46
|
+
|
|
47
|
+
Primary generation stays with the host, between the two pipelines. The host
|
|
48
|
+
keeps the `UserTransaction` from the request pipeline until the response is
|
|
49
|
+
ready. A pipeline step may call an inference endpoint to modify the transaction
|
|
50
|
+
(for example citation rewrite) without the host owning those requests.
|
|
51
|
+
|
|
52
|
+
Both pipelines operate on a `UserTransaction`, which carries the request,
|
|
53
|
+
response, and two per-side context dicts (`request_step_context` and
|
|
54
|
+
`response_step_context`). Requests and responses use the existing
|
|
55
|
+
`neon-data-models` models (`LLMRequest` / `LLMResponse`).
|
|
56
|
+
|
|
57
|
+
## Concepts
|
|
58
|
+
|
|
59
|
+
| Concept | Purpose |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `UserTransaction` | The request/response pair plus `request_step_context` / `response_step_context` dicts. The host holds this object across generation. |
|
|
62
|
+
| `BrainForgeClient` | Builds pipelines from config and exposes `run_request_pipeline` / `run_response_pipeline` / `supported_request_context`. |
|
|
63
|
+
| `RequestStep` | Shapes `transaction.request` before generation. Declares supported `request_context` keys/values. |
|
|
64
|
+
| `ResponseStep` | Annotates/rewrites `transaction.response` after generation. |
|
|
65
|
+
| `RequestContextOption` | One `request_context` key a step reads, with optional allowed values for hosts to present. |
|
|
66
|
+
| `RequestPipeline` / `ResponsePipeline` | Run an ordered list of steps over a transaction. |
|
|
67
|
+
| `PipelineConfig` / `StepConfig` | Declarative, ordered step lists (see below). |
|
|
68
|
+
|
|
69
|
+
## Usage sketch
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
client = BrainForgeClient(pipeline_config)
|
|
73
|
+
|
|
74
|
+
options = await client.supported_request_context()
|
|
75
|
+
# e.g. options["rag_providers"].values, options["conversational"].values
|
|
76
|
+
|
|
77
|
+
transaction = await client.run_request_pipeline(llm_request)
|
|
78
|
+
# host performs generation using transaction.request
|
|
79
|
+
transaction = await client.run_response_pipeline(transaction, llm_response)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Request steps write `transaction.request_step_context[step.id]`. Response steps write
|
|
83
|
+
`transaction.response_step_context[step.id]` and may *read* the request-step context. Those
|
|
84
|
+
namespaces are independent, so the same step id may exist on both sides
|
|
85
|
+
without colliding. Context is available across generation only because the
|
|
86
|
+
same `UserTransaction` is passed back in; this package does not store it
|
|
87
|
+
between calls.
|
|
88
|
+
|
|
89
|
+
## Design notes
|
|
90
|
+
|
|
91
|
+
- **Steps are independently defensive.** There is no dependency graph between
|
|
92
|
+
steps. A step that needs something it does not find (e.g. a citation step with
|
|
93
|
+
no retrieved sources) reports itself inapplicable via `applicable()` and is
|
|
94
|
+
skipped with a logged reason, rather than failing the pipeline.
|
|
95
|
+
- **Ordering is explicit.** Step order is exactly the order declared in
|
|
96
|
+
configuration; the pipeline does not reorder.
|
|
97
|
+
- **Steps may be repeated.** The same step id may appear more than once in a
|
|
98
|
+
pipeline, including with different `config`. Each appearance is a separate
|
|
99
|
+
instance and runs in declared order. The pipeline does not merge or
|
|
100
|
+
de-duplicate entries.
|
|
101
|
+
- **Steps own their context.** Each side has its own dict keyed by step id.
|
|
102
|
+
A repeated step on the *same* side shares that key; a step id on the request
|
|
103
|
+
side does not clash with the same id on the response side. Each step is
|
|
104
|
+
responsible for how it stores and reads its own data across one or more
|
|
105
|
+
calls (for example appending to a list instead of overwriting it).
|
|
106
|
+
Response steps that consume request-step data should read
|
|
107
|
+
`transaction.request_step_context` and report inapplicable when required
|
|
108
|
+
context is absent.
|
|
109
|
+
- **Format contracts live together.** Steps that share a parse format
|
|
110
|
+
(e.g. a citation formatter and the sanitizer that strips it) belong in the
|
|
111
|
+
same module and should derive from a shared constant so they cannot drift.
|
|
112
|
+
|
|
113
|
+
## Step plugins
|
|
114
|
+
|
|
115
|
+
Installed packages advertise steps with setuptools entry points. Request and
|
|
116
|
+
response steps use different groups:
|
|
117
|
+
|
|
118
|
+
| Pipeline | Entry point group |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| Request | `brainforge.inference.request_steps` |
|
|
121
|
+
| Response | `brainforge.inference.response_steps` |
|
|
122
|
+
|
|
123
|
+
Both groups use the step id `author.step_name`:
|
|
124
|
+
|
|
125
|
+
- `author` is the plugin developer's slug (`acme`, `brainforge`).
|
|
126
|
+
- `step_name` names that one step (`guardrails`, `rag`).
|
|
127
|
+
|
|
128
|
+
Each part may contain letters, digits, hyphens, and underscores. The class
|
|
129
|
+
`id`, the config `id`, and the context-dict key are that same string.
|
|
130
|
+
|
|
131
|
+
The same `author.step_name` may be registered once in each group. Each
|
|
132
|
+
pipeline reads only its own group, so the request class and the response class
|
|
133
|
+
stay distinct. Two packages registering the same name in one group is an
|
|
134
|
+
error.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
entry_points={
|
|
138
|
+
"brainforge.inference.request_steps": [
|
|
139
|
+
"acme.guardrails=acme_guardrails:GuardrailRequestStep",
|
|
140
|
+
],
|
|
141
|
+
"brainforge.inference.response_steps": [
|
|
142
|
+
"acme.guardrails=acme_guardrails:GuardrailResponseStep",
|
|
143
|
+
],
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
pipeline:
|
|
149
|
+
request:
|
|
150
|
+
- id: acme.guardrails
|
|
151
|
+
response:
|
|
152
|
+
- id: acme.guardrails
|
|
153
|
+
```
|
|
154
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
brainforge_inference_utils/__init__.py,sha256=EhCt2f4X_swIhULXwmvPrRFDReJjiz0IWZWAm5Nqkbc,3257
|
|
2
|
+
brainforge_inference_utils/client.py,sha256=yGmIlU6Bb9pIt4Oo9vl8m1bhQwpu4diNXGeUPhaWXIY,5398
|
|
3
|
+
brainforge_inference_utils/config.py,sha256=OPLa9G7lTLzad8_tHcC_-K7sWMTfeWZvIfRF9jVmzKg,4491
|
|
4
|
+
brainforge_inference_utils/mcp.py,sha256=TKd884dXkD1X9pSZcy0CaBEuPiXnKuhmvve6iWISg4U,4059
|
|
5
|
+
brainforge_inference_utils/pipeline.py,sha256=2yFYPR0M85XSBNe4qDtxXsoQQKZG0VhM_5382MjwpQg,6877
|
|
6
|
+
brainforge_inference_utils/registry.py,sha256=JGOKyqaGC7wf6Xb9vLIAntTTEq9lcY6bbmjaKXXqx5I,5353
|
|
7
|
+
brainforge_inference_utils/transaction.py,sha256=fIMfLoLxKrwBTF-VLci9R6WdOMS202xAcvqqoOZC1QU,3969
|
|
8
|
+
brainforge_inference_utils/version.py,sha256=qlWZCTABcGFPUiaF907Tq_OSyQNjbvo78jsSyM0NpEU,2004
|
|
9
|
+
brainforge_inference_utils/steps/__init__.py,sha256=e9pQrlsKiK94rHvVy7utDk1qcpHjN5cUStlfhX2YKxs,2374
|
|
10
|
+
brainforge_inference_utils/steps/base.py,sha256=5juJJIeyS-_TVLzSAZaYsZ41qTnCoCxDWVthz-etLrY,8175
|
|
11
|
+
brainforge_inference_utils-0.0.1a1.dist-info/licenses/LICENSE.md,sha256=U4qERlCptYiMPDU7Y_ciX0jYr7DV4IMtvntHL_YAJPM,1635
|
|
12
|
+
brainforge_inference_utils-0.0.1a1.dist-info/METADATA,sha256=FEnsn0nX2jAtdFhBakpluVJtycDJ1IQHAE74qaziFLc,6480
|
|
13
|
+
brainforge_inference_utils-0.0.1a1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
14
|
+
brainforge_inference_utils-0.0.1a1.dist-info/top_level.txt,sha256=MJOy5sqRqWw7qXucFKGEXR7qwEBL6H-hCrfCu1_YV_c,27
|
|
15
|
+
brainforge_inference_utils-0.0.1a1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
16
|
+
brainforge_inference_utils-0.0.1a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System
|
|
2
|
+
# All trademark and other rights reserved by their respective owners
|
|
3
|
+
# Copyright 2008-2026 Neongecko.com Inc.
|
|
4
|
+
# BSD-3 License
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
|
7
|
+
following conditions are met:
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
|
9
|
+
disclaimer.
|
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
|
11
|
+
disclaimer in the documentation and/or other materials provided with the distribution.
|
|
12
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
|
|
13
|
+
derived from this software without specific prior written permission.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
16
|
+
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
17
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
18
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
19
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
20
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
21
|
+
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
brainforge_inference_utils
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|