langchain-mljunction 0.1.0__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,28 @@
1
+ """LangChain integrations for ML Junction."""
2
+
3
+ from langchain_mljunction._client import MLJunctionAPIError
4
+ from langchain_mljunction.chat_models import ChatMLJunction
5
+ from langchain_mljunction.embeddings import MLJunctionEmbeddings
6
+ from langchain_mljunction.outcomes import OutcomeReceipt, Outcomes, request_id_of
7
+ from langchain_mljunction.telemetry import (
8
+ MAX_AGENT_DEPTH,
9
+ MLJunction,
10
+ RunContext,
11
+ expose_agent_as_tool,
12
+ )
13
+ from langchain_mljunction.tracer import MLJunctionTracer
14
+
15
+ __all__ = [
16
+ "MAX_AGENT_DEPTH",
17
+ "ChatMLJunction",
18
+ "MLJunction",
19
+ "MLJunctionAPIError",
20
+ "MLJunctionEmbeddings",
21
+ "MLJunctionTracer",
22
+ "OutcomeReceipt",
23
+ "Outcomes",
24
+ "RunContext",
25
+ "expose_agent_as_tool",
26
+ "request_id_of",
27
+ ]
28
+ __version__ = "0.1.0"
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import AsyncIterator, Iterator
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+
10
+ class MLJunctionAPIError(RuntimeError):
11
+ """Structured gateway failure with status, request ID, code, and details."""
12
+
13
+ def __init__(self, response: httpx.Response) -> None:
14
+ self.status_code = response.status_code
15
+ self.request_id = response.headers.get("x-request-id") or response.headers.get("request-id")
16
+ try:
17
+ body = response.json()
18
+ except ValueError:
19
+ body = {}
20
+ error = body.get("error", body) if isinstance(body, dict) else {}
21
+ self.code = error.get("code") if isinstance(error, dict) else None
22
+ self.error_type = error.get("type") if isinstance(error, dict) else None
23
+ self.details = error.get("details", {}) if isinstance(error, dict) else {}
24
+ message = (
25
+ error.get("message") if isinstance(error, dict) else None
26
+ ) or response.reason_phrase
27
+ super().__init__(
28
+ f"ML Junction request failed ({self.status_code}, {self.code or 'unknown'}): {message}"
29
+ )
30
+
31
+
32
+ def _raise_for_status(response: httpx.Response) -> None:
33
+ if response.is_error:
34
+ if not response.is_closed:
35
+ response.read()
36
+ raise MLJunctionAPIError(response)
37
+
38
+
39
+ async def _araise_for_status(response: httpx.Response) -> None:
40
+ if response.is_error:
41
+ if not response.is_closed:
42
+ await response.aread()
43
+ raise MLJunctionAPIError(response)
44
+
45
+
46
+ class MLJunctionClient:
47
+ """Small native transport shared by LangChain integrations."""
48
+
49
+ def __init__(
50
+ self, *, api_key: str, base_url: str, timeout: float, app_name: str | None = None
51
+ ) -> None:
52
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
53
+ if app_name:
54
+ headers["X-App"] = app_name
55
+ self.sync = httpx.Client(base_url=base_url.rstrip("/"), headers=headers, timeout=timeout)
56
+ self.async_ = httpx.AsyncClient(
57
+ base_url=base_url.rstrip("/"), headers=headers, timeout=timeout
58
+ )
59
+
60
+ def post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
61
+ response = self.sync.post(path, json=payload)
62
+ _raise_for_status(response)
63
+ return response.json()
64
+
65
+ async def apost(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
66
+ response = await self.async_.post(path, json=payload)
67
+ _raise_for_status(response)
68
+ return response.json()
69
+
70
+ def put(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
71
+ """Upsert. Used by outcome-definition registration, which is idempotent
72
+ by name rather than creating a new row per call."""
73
+ response = self.sync.put(path, json=payload)
74
+ _raise_for_status(response)
75
+ return response.json()
76
+
77
+ async def aput(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
78
+ response = await self.async_.put(path, json=payload)
79
+ await _araise_for_status(response)
80
+ return response.json()
81
+
82
+ def stream(self, path: str, payload: dict[str, Any]) -> Iterator[tuple[str, dict[str, Any]]]:
83
+ with self.sync.stream("POST", path, json=payload) as response:
84
+ _raise_for_status(response)
85
+ yield from _parse_sse(response.iter_lines())
86
+
87
+ async def astream(
88
+ self, path: str, payload: dict[str, Any]
89
+ ) -> AsyncIterator[tuple[str, dict[str, Any]]]:
90
+ async with self.async_.stream("POST", path, json=payload) as response:
91
+ await _araise_for_status(response)
92
+ event: str | None = None
93
+ data: list[str] = []
94
+ async for line in response.aiter_lines():
95
+ if line.startswith("event:"):
96
+ event = line[6:].strip()
97
+ elif line.startswith("data:"):
98
+ data.append(line[5:].strip())
99
+ elif not line and event and data:
100
+ yield event, json.loads("\n".join(data))
101
+ event, data = None, []
102
+
103
+ def close(self) -> None:
104
+ self.sync.close()
105
+
106
+ async def aclose(self) -> None:
107
+ await self.async_.aclose()
108
+
109
+
110
+ def _parse_sse(lines: Iterator[str]) -> Iterator[tuple[str, dict[str, Any]]]:
111
+ event: str | None = None
112
+ data: list[str] = []
113
+ for line in lines:
114
+ if line.startswith("event:"):
115
+ event = line[6:].strip()
116
+ elif line.startswith("data:"):
117
+ data.append(line[5:].strip())
118
+ elif not line and event and data:
119
+ yield event, json.loads("\n".join(data))
120
+ event, data = None, []