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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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