langchain-mljunction 0.1.0__tar.gz
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.
- langchain_mljunction-0.1.0/.github/workflows/release.yml +105 -0
- langchain_mljunction-0.1.0/.gitignore +9 -0
- langchain_mljunction-0.1.0/LICENSE +21 -0
- langchain_mljunction-0.1.0/PKG-INFO +327 -0
- langchain_mljunction-0.1.0/README.md +291 -0
- langchain_mljunction-0.1.0/docs/assets/readme-hero.png +0 -0
- langchain_mljunction-0.1.0/pyproject.toml +75 -0
- langchain_mljunction-0.1.0/src/langchain_mljunction/__init__.py +28 -0
- langchain_mljunction-0.1.0/src/langchain_mljunction/_client.py +120 -0
- langchain_mljunction-0.1.0/src/langchain_mljunction/chat_models.py +651 -0
- langchain_mljunction-0.1.0/src/langchain_mljunction/embeddings.py +77 -0
- langchain_mljunction-0.1.0/src/langchain_mljunction/outcomes.py +305 -0
- langchain_mljunction-0.1.0/src/langchain_mljunction/telemetry.py +287 -0
- langchain_mljunction-0.1.0/src/langchain_mljunction/tracer.py +901 -0
- langchain_mljunction-0.1.0/tests/cassettes/TestChatMLJunctionIntegration.test_stream_time.yaml +74 -0
- langchain_mljunction-0.1.0/tests/conftest.py +24 -0
- langchain_mljunction-0.1.0/tests/standard_tests/test_chat_models_integration.py +74 -0
- langchain_mljunction-0.1.0/tests/standard_tests/test_chat_models_unit.py +34 -0
- langchain_mljunction-0.1.0/tests/standard_tests/test_vcr_cassette.py +25 -0
- langchain_mljunction-0.1.0/tests/test_chat_models.py +449 -0
- langchain_mljunction-0.1.0/tests/test_live.py +149 -0
- langchain_mljunction-0.1.0/tests/test_outcomes.py +153 -0
- langchain_mljunction-0.1.0/tests/test_tracer.py +314 -0
- langchain_mljunction-0.1.0/uv.lock +1864 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Manual only: Actions > Release > Run workflow.
|
|
4
|
+
# Builds the package from the chosen ref and uploads it to PyPI. The version published is
|
|
5
|
+
# whatever pyproject.toml says at that ref, so bump it (and __init__.py) before running.
|
|
6
|
+
# Upload uses PyPI Trusted Publishing (OIDC), so no API token is stored in this repo.
|
|
7
|
+
on:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
inputs:
|
|
10
|
+
ref:
|
|
11
|
+
description: "Branch, tag or commit SHA to release"
|
|
12
|
+
type: string
|
|
13
|
+
default: master
|
|
14
|
+
|
|
15
|
+
# One release at a time, and never cancel one midway.
|
|
16
|
+
concurrency:
|
|
17
|
+
group: release-pypi
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
build:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
timeout-minutes: 20
|
|
27
|
+
# Every step below runs inside this image (Astral's official uv + Python), not on
|
|
28
|
+
# the runner, so nothing about the build depends on what Ubuntu happens to ship.
|
|
29
|
+
container: ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
|
30
|
+
outputs:
|
|
31
|
+
version: ${{ steps.version.outputs.version }}
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
with:
|
|
35
|
+
ref: ${{ inputs.ref }}
|
|
36
|
+
persist-credentials: false
|
|
37
|
+
|
|
38
|
+
- name: Check the version is consistent and not already on PyPI
|
|
39
|
+
id: version
|
|
40
|
+
run: |
|
|
41
|
+
set -eu
|
|
42
|
+
project=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)
|
|
43
|
+
module=$(grep -m1 '^__version__ = ' src/langchain_mljunction/__init__.py | cut -d'"' -f2)
|
|
44
|
+
echo "pyproject.toml=$project __init__.py=$module"
|
|
45
|
+
if [ "$project" != "$module" ]; then
|
|
46
|
+
echo "::error::pyproject.toml ($project) and __init__.py ($module) disagree. Bump both."
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
# PyPI never accepts the same version twice, so fail here, before the tests,
|
|
50
|
+
# rather than at the upload.
|
|
51
|
+
status=$(python -c "import sys, urllib.request, urllib.error
|
|
52
|
+
try:
|
|
53
|
+
urllib.request.urlopen('https://pypi.org/pypi/langchain-mljunction/$project/json')
|
|
54
|
+
print('taken')
|
|
55
|
+
except urllib.error.HTTPError as e:
|
|
56
|
+
print('free' if e.code == 404 else f'error {e.code}')")
|
|
57
|
+
if [ "$status" != "free" ]; then
|
|
58
|
+
echo "::error::Version $project is not publishable on PyPI ($status). Bump the version."
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
echo "version=$project" >> "$GITHUB_OUTPUT"
|
|
62
|
+
|
|
63
|
+
- run: uv sync --locked
|
|
64
|
+
# Live tests skip themselves: no RUN_LIVE_PROVIDER_TESTS or LIVE_API_KEY here.
|
|
65
|
+
- run: uv run pytest -q
|
|
66
|
+
- run: uv build
|
|
67
|
+
- run: uvx twine check --strict dist/*
|
|
68
|
+
- uses: actions/upload-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: dist
|
|
71
|
+
path: dist/
|
|
72
|
+
|
|
73
|
+
publish:
|
|
74
|
+
needs: build
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
timeout-minutes: 10
|
|
77
|
+
# The PyPI trusted publisher is bound to this environment name. Required reviewers
|
|
78
|
+
# set on it in repo settings become an approval gate before the upload.
|
|
79
|
+
environment:
|
|
80
|
+
name: pypi
|
|
81
|
+
url: https://pypi.org/project/langchain-mljunction/${{ needs.build.outputs.version }}/
|
|
82
|
+
permissions:
|
|
83
|
+
id-token: write
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/download-artifact@v4
|
|
86
|
+
with:
|
|
87
|
+
name: dist
|
|
88
|
+
path: dist/
|
|
89
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
90
|
+
|
|
91
|
+
summary:
|
|
92
|
+
needs: [build, publish]
|
|
93
|
+
if: always()
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
steps:
|
|
96
|
+
- name: Summary
|
|
97
|
+
run: |
|
|
98
|
+
{
|
|
99
|
+
echo "### Release to PyPI"
|
|
100
|
+
echo ""
|
|
101
|
+
echo "- Ref: \`${{ inputs.ref }}\`"
|
|
102
|
+
echo "- Version: ${{ needs.build.outputs.version || 'unknown' }}"
|
|
103
|
+
echo "- Build: ${{ needs.build.result }}"
|
|
104
|
+
echo "- Publish: ${{ needs.publish.result }}"
|
|
105
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 UGIBO (ML Junction)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: langchain-mljunction
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Native LangChain integration for the ML Junction gateway
|
|
5
|
+
Project-URL: Homepage, https://mljunction.com
|
|
6
|
+
Project-URL: Repository, https://github.com/mljunction/langchain-mljunction
|
|
7
|
+
Project-URL: Issues, https://github.com/mljunction/langchain-mljunction/issues
|
|
8
|
+
Author-email: ML Junction <mail@mljunction.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: langchain,llm,llm-gateway,llm-router,mljunction
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: <4,>=3.10
|
|
22
|
+
Requires-Dist: httpx<1,>=0.27
|
|
23
|
+
Requires-Dist: langchain-core<2,>=0.3.70
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.9
|
|
25
|
+
Provides-Extra: otel
|
|
26
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.36; extra == 'otel'
|
|
27
|
+
Requires-Dist: opentelemetry-sdk<2,>=1.36; extra == 'otel'
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: langchain-tests<2,>=1.1.9; extra == 'test'
|
|
30
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.36; extra == 'test'
|
|
31
|
+
Requires-Dist: opentelemetry-sdk<2,>=1.36; extra == 'test'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=1.0; extra == 'test'
|
|
33
|
+
Requires-Dist: pytest>=8.4; extra == 'test'
|
|
34
|
+
Requires-Dist: ruff>=0.12; extra == 'test'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
<p align="center"><img src="https://raw.githubusercontent.com/mljunction/langchain-mljunction/master/docs/assets/readme-hero.png" alt="langchain-mljunction - LangChain, routed" width="100%"></p>
|
|
38
|
+
|
|
39
|
+
# langchain-mljunction
|
|
40
|
+
|
|
41
|
+
The native LangChain integration for ML Junction. It calls `/v1/responses` and `/v1/embeddings`
|
|
42
|
+
directly—there is no `ChatOpenAI` wrapper and no vendor-shaped ceiling between your chain and ML
|
|
43
|
+
Junction's routing, governance, privacy, billing, and observability features.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install langchain-mljunction
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For local SDK development, from a checkout:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/mljunction/langchain-mljunction.git
|
|
55
|
+
cd langchain-mljunction
|
|
56
|
+
pip install -e ".[test]"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Agent tracing uses the optional OpenTelemetry transport:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install "langchain-mljunction[otel]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Set `MLJUNCTION_API_KEY` and optionally `MLJUNCTION_BASE_URL`, or pass both to the constructor.
|
|
66
|
+
The default base URL is `https://api.mljunction.com`. Point `MLJUNCTION_BASE_URL` at
|
|
67
|
+
`http://localhost:8001` to use a local ML Junction server instead.
|
|
68
|
+
|
|
69
|
+
## Start in 30 seconds
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from langchain_mljunction import ChatMLJunction
|
|
73
|
+
|
|
74
|
+
llm = ChatMLJunction(
|
|
75
|
+
model="gpt-5-mini",
|
|
76
|
+
routing={
|
|
77
|
+
"strategy": "balanced",
|
|
78
|
+
"service_tier": "auto",
|
|
79
|
+
"require_zdr": True,
|
|
80
|
+
"max_platform_charge_usd": 0.05,
|
|
81
|
+
},
|
|
82
|
+
app_name="support-agent",
|
|
83
|
+
session_name="customer-42",
|
|
84
|
+
task_name="triage-ticket",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
answer = llm.invoke("Explain the incident in three bullets")
|
|
88
|
+
print(answer.content)
|
|
89
|
+
print(answer.response_metadata["routing"])
|
|
90
|
+
print(answer.response_metadata["receipt"])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Native controls, first class
|
|
94
|
+
|
|
95
|
+
`ChatMLJunction` exposes the native request surface: sampling controls, `reasoning`, `output`,
|
|
96
|
+
`requirements`, `routing`, `context`, `metadata`, `idempotency_key`, session/task identity, and
|
|
97
|
+
`compatibility`. Per-call overrides work with `invoke`, `stream`, `batch`, and async equivalents.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
llm = ChatMLJunction(
|
|
101
|
+
model="gpt-5-mini",
|
|
102
|
+
reasoning={"enabled": True, "effort": "high"},
|
|
103
|
+
requirements={"reasoning": "required", "tools": "preferred"},
|
|
104
|
+
routing={
|
|
105
|
+
"strategy": "quality",
|
|
106
|
+
"mode": "platform_only",
|
|
107
|
+
"fallbacks": {"provider": True, "model": True},
|
|
108
|
+
},
|
|
109
|
+
context={"mode": "strict", "preserve_tool_chains": True},
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Provider-native reasoning details are retained in
|
|
114
|
+
`AIMessage.additional_kwargs["reasoning_details"]` and replayed on that exact assistant message.
|
|
115
|
+
The encrypted `gwrt_v3` routing token is retained in `response_metadata["reasoning"]` and is
|
|
116
|
+
automatically supplied with the next full message history. Pass
|
|
117
|
+
`reasoning={"continuation_token": None}` to intentionally abandon replay for a fork. The token
|
|
118
|
+
contains routing affinity only; it does not contain conversation history, reasoning, or credentials.
|
|
119
|
+
|
|
120
|
+
## Tools, structured output, and streaming
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from langchain_core.messages import HumanMessage, ToolMessage
|
|
124
|
+
from langchain_core.tools import tool
|
|
125
|
+
from pydantic import BaseModel
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Decision(BaseModel):
|
|
129
|
+
action: str
|
|
130
|
+
confidence: float
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
decision = llm.with_structured_output(Decision).invoke("Choose: retry or escalate")
|
|
134
|
+
print(decision)
|
|
135
|
+
|
|
136
|
+
# LangChain's standard method selector is supported.
|
|
137
|
+
decision = llm.with_structured_output(
|
|
138
|
+
Decision,
|
|
139
|
+
method="json_schema", # also "function_calling" or "json_mode"
|
|
140
|
+
).invoke("Choose: retry or escalate")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@tool
|
|
144
|
+
def lookup_customer(customer_id: str) -> str:
|
|
145
|
+
"""Look up a customer account."""
|
|
146
|
+
return f"Customer {customer_id} is active"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
tool_llm = llm.bind_tools(
|
|
150
|
+
[lookup_customer],
|
|
151
|
+
tool_choice="auto",
|
|
152
|
+
strict=True,
|
|
153
|
+
parallel_tool_calls=True,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
messages = [HumanMessage("Check account 42")]
|
|
157
|
+
assistant = tool_llm.invoke(messages)
|
|
158
|
+
messages.append(assistant)
|
|
159
|
+
|
|
160
|
+
for call in assistant.tool_calls:
|
|
161
|
+
result = lookup_customer.invoke(call["args"])
|
|
162
|
+
messages.append(ToolMessage(result, tool_call_id=call["id"]))
|
|
163
|
+
|
|
164
|
+
final = tool_llm.invoke(messages)
|
|
165
|
+
print(final.content)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Text and ordinary bound tool-call arguments can also be consumed incrementally:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
for chunk in tool_llm.stream("Check accounts 42 and 84"):
|
|
172
|
+
if chunk.content:
|
|
173
|
+
print(chunk.content, end="", flush=True)
|
|
174
|
+
for call in chunk.tool_call_chunks:
|
|
175
|
+
print("tool delta:", call)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Sync/async invocation, text streaming, streamed tool arguments, tool binding, strict JSON Schema,
|
|
179
|
+
parallel tool calls, multimodal content, and LangSmith metadata are supported. Tool execution remains
|
|
180
|
+
the application's responsibility: append each `ToolMessage` and invoke the model again. ML Junction
|
|
181
|
+
reasoning continuation automatically marks that follow-up as tool results, even when workflow
|
|
182
|
+
messages appear after the tool result.
|
|
183
|
+
|
|
184
|
+
Multimodal input accepts LangChain standard blocks and the common OpenAI/Anthropic-compatible forms
|
|
185
|
+
for image URLs/base64 images, base64 audio, PDFs/files, multimodal tool results, cache-control text,
|
|
186
|
+
and thinking blocks. The adapter normalizes them to ML Junction's provider-neutral message contract.
|
|
187
|
+
|
|
188
|
+
Structured-output runnables deliberately coerce `stream()` and `astream()` to one complete
|
|
189
|
+
`AIMessageChunk`. This applies to native JSON Schema/JSON mode and to function-calling used as a
|
|
190
|
+
structured-output method, so a LangChain parser never sees partial JSON. Ordinary
|
|
191
|
+
`bind_tools().stream()` calls remain incremental and LangChain reassembles their argument deltas.
|
|
192
|
+
|
|
193
|
+
`include_raw=True` follows LangChain's standard result contract and returns a dictionary containing
|
|
194
|
+
`raw`, `parsed`, and `parsing_error`. The complete unparsed `AIMessage` is available under `raw`;
|
|
195
|
+
read its `content` for native JSON Schema/JSON mode or `tool_calls` for function-calling.
|
|
196
|
+
|
|
197
|
+
## Sync, async, batch, and per-call overrides
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
answer = llm.invoke("Hello")
|
|
201
|
+
answer = await llm.ainvoke("Hello")
|
|
202
|
+
|
|
203
|
+
answers = llm.batch(["One", "Two"])
|
|
204
|
+
answers = await llm.abatch(["One", "Two"])
|
|
205
|
+
|
|
206
|
+
high_effort = llm.invoke(
|
|
207
|
+
"Analyze this carefully",
|
|
208
|
+
reasoning={"enabled": True, "effort": "high"},
|
|
209
|
+
session_id="analysis-session-1",
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Use a stable `session_id` for related requests when you want sticky routing and coherent request
|
|
214
|
+
grouping in ML Junction's activity logs.
|
|
215
|
+
|
|
216
|
+
## Embeddings
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from langchain_mljunction import MLJunctionEmbeddings
|
|
220
|
+
|
|
221
|
+
embeddings = MLJunctionEmbeddings(
|
|
222
|
+
model="text-embedding-3-small",
|
|
223
|
+
dimensions=1024,
|
|
224
|
+
routing={"strategy": "latency", "max_platform_charge_usd": 0.01},
|
|
225
|
+
)
|
|
226
|
+
vectors = embeddings.embed_documents(["route", "receipt", "reasoning"])
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## OpenTelemetry agent tracing
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from langchain_mljunction import MLJunction
|
|
233
|
+
|
|
234
|
+
telemetry = MLJunction(
|
|
235
|
+
api_key="mlj_...",
|
|
236
|
+
base_url="https://api.mljunction.com",
|
|
237
|
+
app_id="support-bot",
|
|
238
|
+
)
|
|
239
|
+
run = telemetry.context(agent_name="coordinator", session_id="conversation-42")
|
|
240
|
+
agent.invoke({"messages": [...]}, config=run.config)
|
|
241
|
+
telemetry.flush()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The callback bridge emits standard OTLP spans to `/v1/traces`, reuses an
|
|
245
|
+
existing global OpenTelemetry provider when present, and supports dual export.
|
|
246
|
+
Set `export_inflight=True` only when debugging hung agents; it emits an extra
|
|
247
|
+
start snapshot for each span and is off by default.
|
|
248
|
+
|
|
249
|
+
Every `AIMessage` carries standard `usage_metadata`. `response_metadata` retains request ID,
|
|
250
|
+
model, routing, receipt, warnings, session/task/app identity, and reasoning state—nothing important
|
|
251
|
+
is discarded to imitate another provider.
|
|
252
|
+
|
|
253
|
+
## Reporting outcomes
|
|
254
|
+
|
|
255
|
+
An adaptive model learns which candidate to route to from evidence. Besides offline
|
|
256
|
+
evaluation suites, your application can report what actually happened afterwards: a
|
|
257
|
+
ticket closing, a patch passing its tests, a meeting getting booked. ML Junction never
|
|
258
|
+
interprets a metric's name; it compares each one against itself across the models in a
|
|
259
|
+
pool.
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
from langchain_mljunction import ChatMLJunction, Outcomes, request_id_of
|
|
263
|
+
|
|
264
|
+
# One-time: register each metric name. Needs a management key with adaptive:write
|
|
265
|
+
# (management_api_key= or MLJUNCTION_MANAGEMENT_API_KEY).
|
|
266
|
+
outcomes = Outcomes()
|
|
267
|
+
outcomes.register("resolved", direction="maximize")
|
|
268
|
+
outcomes.register("csat", direction="maximize", lower_bound=1, upper_bound=5)
|
|
269
|
+
|
|
270
|
+
# A support ticket: many requests, one outcome, so report it on the session.
|
|
271
|
+
chat = ChatMLJunction(model="support-agent", session_id="ticket_8842")
|
|
272
|
+
reply = chat.invoke("I want a refund")
|
|
273
|
+
|
|
274
|
+
receipt = outcomes.report(
|
|
275
|
+
session_id="ticket_8842",
|
|
276
|
+
event_id="ticket_8842_closed", # your idempotency key
|
|
277
|
+
metrics={"resolved": True, "csat": 4.8},
|
|
278
|
+
)
|
|
279
|
+
print(receipt.accepted, receipt.duplicates, receipt.discarded)
|
|
280
|
+
|
|
281
|
+
# One-shot work is reported on the request itself.
|
|
282
|
+
outcomes.report(request_id=request_id_of(reply), event_id="evt_1", metrics={"resolved": True})
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Three rules:
|
|
286
|
+
|
|
287
|
+
- **Register a name before sending it.** An unregistered name is refused, so a typo
|
|
288
|
+
cannot quietly become a second metric.
|
|
289
|
+
- **`event_id` is required and is yours.** A report retried by a queue is counted once
|
|
290
|
+
(`receipt.duplicates`), not twice.
|
|
291
|
+
- **Report against exactly one target**: `request_id`, `session_id` or `task_id`,
|
|
292
|
+
whichever unit the outcome is about.
|
|
293
|
+
|
|
294
|
+
Reporting uses the ordinary inference key. Queue-flushed reports go in one round trip
|
|
295
|
+
with `report_batch([...])`, where each entry takes the same arguments as `report`. Pass
|
|
296
|
+
`observed_at=` when the event happened earlier than the call. `areport`,
|
|
297
|
+
`areport_batch` and `aregister` are the async forms. `discarded` in the receipt is not
|
|
298
|
+
an error: it counts outcomes that could not be attributed to a single candidate.
|
|
299
|
+
|
|
300
|
+
## Errors and transport lifecycle
|
|
301
|
+
|
|
302
|
+
API failures raise `MLJunctionAPIError` with the HTTP status and ML Junction error payload. Streaming
|
|
303
|
+
errors are read and decoded before the exception is raised, for both sync and async clients.
|
|
304
|
+
|
|
305
|
+
Long-lived applications should close the owned HTTP transports:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
llm.close()
|
|
309
|
+
|
|
310
|
+
# Async applications:
|
|
311
|
+
await llm.aclose()
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Development
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
pip install -e ".[test]"
|
|
318
|
+
pytest
|
|
319
|
+
ruff check src tests
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Live tests are opt-in with `RUN_LIVE_PROVIDER_TESTS=1`, `LIVE_API_KEY`, and `LIVE_API_BASE`.
|
|
323
|
+
The `tests/standard_tests` directory subclasses LangChain's `ChatModelUnitTests` and
|
|
324
|
+
`ChatModelIntegrationTests`; run it whenever the supported LangChain contract changes.
|
|
325
|
+
The stream timing test uses a sanitized cassette in `tests/cassettes` and can be verified with
|
|
326
|
+
`--record-mode=none`. Its replay has also been validated with the gateway stopped and pytest sockets
|
|
327
|
+
disabled, ensuring routine benchmark runs cannot make an accidental provider request.
|