switchlane 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.
@@ -0,0 +1,14 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ *.log
5
+ .DS_Store
6
+ .venv/
7
+ __pycache__/
8
+ .pytest_cache/
9
+ *.egg-info/
10
+ python/build/
11
+ python/dist/
12
+
13
+ # Local-only files
14
+ local/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Troia Labs
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,50 @@
1
+ Metadata-Version: 2.5
2
+ Name: switchlane
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Switchlane runtime agent routing
5
+ Project-URL: Homepage, https://router.troialabs.ai
6
+ Project-URL: Repository, https://github.com/troailabs/switchlane
7
+ Project-URL: Issues, https://github.com/troailabs/switchlane/issues
8
+ Author: Troia Labs
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,mcp,multi-agent,routing
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx<1,>=0.27
22
+ Requires-Dist: pydantic<3,>=2.6
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest<9,>=8; extra == 'test'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Switchlane Python SDK
28
+
29
+ A typed sync and async client for the Switchlane runtime routing API.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install switchlane
35
+ ```
36
+
37
+ ## Use
38
+
39
+ ```python
40
+ from switchlane import Switchlane
41
+
42
+ with Switchlane("sl_live_...") as client:
43
+ result = client.route("Review this pull request")
44
+ if result.meta.abstained:
45
+ print(result.meta.abstention_reason)
46
+ else:
47
+ print(result.recommendations[0].agent_id)
48
+ ```
49
+
50
+ This package is an API client, not an agent framework. The SDK is MIT licensed; the Switchlane server is AGPL-3.0-only.
@@ -0,0 +1,24 @@
1
+ # Switchlane Python SDK
2
+
3
+ A typed sync and async client for the Switchlane runtime routing API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install switchlane
9
+ ```
10
+
11
+ ## Use
12
+
13
+ ```python
14
+ from switchlane import Switchlane
15
+
16
+ with Switchlane("sl_live_...") as client:
17
+ result = client.route("Review this pull request")
18
+ if result.meta.abstained:
19
+ print(result.meta.abstention_reason)
20
+ else:
21
+ print(result.recommendations[0].agent_id)
22
+ ```
23
+
24
+ This package is an API client, not an agent framework. The SDK is MIT licensed; the Switchlane server is AGPL-3.0-only.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "switchlane"
7
+ version = "0.1.0"
8
+ description = "Python SDK for Switchlane runtime agent routing"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Troia Labs" }]
13
+ keywords = ["ai", "agents", "routing", "mcp", "multi-agent"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = [
25
+ "httpx>=0.27,<1",
26
+ "pydantic>=2.6,<3",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://router.troialabs.ai"
31
+ Repository = "https://github.com/troailabs/switchlane"
32
+ Issues = "https://github.com/troailabs/switchlane/issues"
33
+
34
+ [project.optional-dependencies]
35
+ test = ["pytest>=8,<9"]
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/switchlane"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
@@ -0,0 +1,29 @@
1
+ from .client import AsyncSwitchlane, Switchlane, SwitchlaneError
2
+ from .models import (
3
+ Agent,
4
+ AgentListResponse,
5
+ ExecutionResult,
6
+ FeedbackResponse,
7
+ Recommendation,
8
+ RouteConstraints,
9
+ RouteMeta,
10
+ RouteResponse,
11
+ TaskProfile,
12
+ UsageResponse,
13
+ )
14
+
15
+ __all__ = [
16
+ "Agent",
17
+ "AgentListResponse",
18
+ "AsyncSwitchlane",
19
+ "ExecutionResult",
20
+ "FeedbackResponse",
21
+ "Recommendation",
22
+ "RouteConstraints",
23
+ "RouteMeta",
24
+ "RouteResponse",
25
+ "Switchlane",
26
+ "SwitchlaneError",
27
+ "TaskProfile",
28
+ "UsageResponse",
29
+ ]
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from .models import (
9
+ AgentListResponse,
10
+ FeedbackResponse,
11
+ RouteConstraints,
12
+ RouteResponse,
13
+ UsageResponse,
14
+ )
15
+
16
+ DEFAULT_BASE_URL = "https://router.troialabs.ai"
17
+
18
+
19
+ class SwitchlaneError(Exception):
20
+ def __init__(self, status_code: int, message: str, body: Any = None) -> None:
21
+ super().__init__(message)
22
+ self.status_code = status_code
23
+ self.body = body
24
+
25
+
26
+ def _raise_for_status(response: httpx.Response) -> None:
27
+ if response.is_success:
28
+ return
29
+ try:
30
+ body = response.json()
31
+ except ValueError:
32
+ body = {"error": response.text or response.reason_phrase}
33
+ message = body.get("error", response.reason_phrase) if isinstance(body, dict) else response.reason_phrase
34
+ raise SwitchlaneError(response.status_code, str(message), body)
35
+
36
+
37
+ def _route_payload(
38
+ task: str,
39
+ *,
40
+ input: Mapping[str, Any] | None,
41
+ constraints: RouteConstraints | Mapping[str, Any] | None,
42
+ execute: bool,
43
+ limit: int,
44
+ ) -> dict[str, Any]:
45
+ constraint_data = constraints.model_dump(exclude_none=True) if isinstance(constraints, RouteConstraints) else constraints
46
+ return {
47
+ "task": task,
48
+ "input": dict(input) if input is not None else None,
49
+ "constraints": dict(constraint_data) if constraint_data is not None else None,
50
+ "execute": execute,
51
+ "limit": limit,
52
+ }
53
+
54
+
55
+ class Switchlane:
56
+ def __init__(
57
+ self,
58
+ api_key: str,
59
+ *,
60
+ base_url: str = DEFAULT_BASE_URL,
61
+ timeout: float = 30.0,
62
+ transport: httpx.BaseTransport | None = None,
63
+ ) -> None:
64
+ self._client = httpx.Client(
65
+ base_url=base_url.rstrip("/"),
66
+ timeout=timeout,
67
+ transport=transport,
68
+ headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
69
+ )
70
+
71
+ def close(self) -> None:
72
+ self._client.close()
73
+
74
+ def __enter__(self) -> Switchlane:
75
+ return self
76
+
77
+ def __exit__(self, *_: object) -> None:
78
+ self.close()
79
+
80
+ def route(
81
+ self,
82
+ task: str,
83
+ *,
84
+ input: Mapping[str, Any] | None = None,
85
+ constraints: RouteConstraints | Mapping[str, Any] | None = None,
86
+ execute: bool = False,
87
+ limit: int = 5,
88
+ ) -> RouteResponse:
89
+ response = self._client.post(
90
+ "/v1/route",
91
+ json=_route_payload(task, input=input, constraints=constraints, execute=execute, limit=limit),
92
+ )
93
+ _raise_for_status(response)
94
+ return RouteResponse.model_validate(response.json())
95
+
96
+ def execute(
97
+ self,
98
+ task: str,
99
+ input: Mapping[str, Any] | None = None,
100
+ *,
101
+ constraints: RouteConstraints | Mapping[str, Any] | None = None,
102
+ limit: int = 5,
103
+ ) -> RouteResponse:
104
+ return self.route(task, input=input, constraints=constraints, execute=True, limit=limit)
105
+
106
+ def list_agents(self, **params: Any) -> AgentListResponse:
107
+ response = self._client.get("/v1/agents", params={key: value for key, value in params.items() if value is not None})
108
+ _raise_for_status(response)
109
+ return AgentListResponse.model_validate(response.json())
110
+
111
+ def feedback(self, agent_id: str, score: float, *, task_id: str | None = None, comment: str | None = None) -> FeedbackResponse:
112
+ response = self._client.post(
113
+ "/v1/feedback",
114
+ json={"agent_id": agent_id, "score": score, "task_id": task_id, "comment": comment},
115
+ )
116
+ _raise_for_status(response)
117
+ return FeedbackResponse.model_validate(response.json())
118
+
119
+ def usage(self) -> UsageResponse:
120
+ response = self._client.get("/v1/billing/usage")
121
+ _raise_for_status(response)
122
+ return UsageResponse.model_validate(response.json())
123
+
124
+
125
+ class AsyncSwitchlane:
126
+ def __init__(
127
+ self,
128
+ api_key: str,
129
+ *,
130
+ base_url: str = DEFAULT_BASE_URL,
131
+ timeout: float = 30.0,
132
+ transport: httpx.AsyncBaseTransport | None = None,
133
+ ) -> None:
134
+ self._client = httpx.AsyncClient(
135
+ base_url=base_url.rstrip("/"),
136
+ timeout=timeout,
137
+ transport=transport,
138
+ headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
139
+ )
140
+
141
+ async def close(self) -> None:
142
+ await self._client.aclose()
143
+
144
+ async def __aenter__(self) -> AsyncSwitchlane:
145
+ return self
146
+
147
+ async def __aexit__(self, *_: object) -> None:
148
+ await self.close()
149
+
150
+ async def route(
151
+ self,
152
+ task: str,
153
+ *,
154
+ input: Mapping[str, Any] | None = None,
155
+ constraints: RouteConstraints | Mapping[str, Any] | None = None,
156
+ execute: bool = False,
157
+ limit: int = 5,
158
+ ) -> RouteResponse:
159
+ response = await self._client.post(
160
+ "/v1/route",
161
+ json=_route_payload(task, input=input, constraints=constraints, execute=execute, limit=limit),
162
+ )
163
+ _raise_for_status(response)
164
+ return RouteResponse.model_validate(response.json())
165
+
166
+ async def execute(
167
+ self,
168
+ task: str,
169
+ input: Mapping[str, Any] | None = None,
170
+ *,
171
+ constraints: RouteConstraints | Mapping[str, Any] | None = None,
172
+ limit: int = 5,
173
+ ) -> RouteResponse:
174
+ return await self.route(task, input=input, constraints=constraints, execute=True, limit=limit)
175
+
176
+ async def list_agents(self, **params: Any) -> AgentListResponse:
177
+ response = await self._client.get("/v1/agents", params={key: value for key, value in params.items() if value is not None})
178
+ _raise_for_status(response)
179
+ return AgentListResponse.model_validate(response.json())
180
+
181
+ async def feedback(self, agent_id: str, score: float, *, task_id: str | None = None, comment: str | None = None) -> FeedbackResponse:
182
+ response = await self._client.post(
183
+ "/v1/feedback",
184
+ json={"agent_id": agent_id, "score": score, "task_id": task_id, "comment": comment},
185
+ )
186
+ _raise_for_status(response)
187
+ return FeedbackResponse.model_validate(response.json())
188
+
189
+ async def usage(self) -> UsageResponse:
190
+ response = await self._client.get("/v1/billing/usage")
191
+ _raise_for_status(response)
192
+ return UsageResponse.model_validate(response.json())
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class Model(BaseModel):
9
+ model_config = ConfigDict(extra="allow")
10
+
11
+
12
+ class RouteConstraints(Model):
13
+ max_latency_ms: int | None = Field(default=None, gt=0)
14
+ max_cost_usd: float | None = Field(default=None, gt=0)
15
+ min_quality_score: float | None = Field(default=None, ge=0, le=1)
16
+ quality_weight: float | None = Field(default=None, ge=0, le=1)
17
+ cost_weight: float | None = Field(default=None, ge=0, le=1)
18
+ latency_weight: float | None = Field(default=None, ge=0, le=1)
19
+ min_routing_confidence: float | None = Field(default=None, ge=0, le=1)
20
+
21
+
22
+ class Recommendation(Model):
23
+ agent_id: str
24
+ provider: str
25
+ quality_score: float
26
+ estimated_cost_usd: float | None = None
27
+ estimated_latency_ms: int | None = None
28
+ match_reason: str
29
+ endpoint: str
30
+
31
+
32
+ class ExecutionResult(Model):
33
+ agent_id: str
34
+ agent_name: str
35
+ tool_used: str | None = None
36
+ success: bool
37
+ content: Any = None
38
+ error: str | None = None
39
+ latency_ms: int
40
+
41
+
42
+ class TaskProfile(Model):
43
+ category: str
44
+ subcategory: str | None = None
45
+ language: str | None = None
46
+ input_type: str | None = None
47
+ output_type: str | None = None
48
+ complexity: Literal["simple", "medium", "complex"]
49
+ keywords: list[str] = Field(default_factory=list)
50
+
51
+
52
+ class RouteMeta(Model):
53
+ match_path: str
54
+ candidates_evaluated: int
55
+ elapsed_ms: int
56
+ abstained: bool
57
+ abstention_reason: Literal[
58
+ "no_candidates",
59
+ "constraints_filtered_all_candidates",
60
+ "top_candidate_below_confidence_threshold",
61
+ ] | None = None
62
+ confidence: float | None = None
63
+
64
+
65
+ class RouteResponse(Model):
66
+ recommendations: list[Recommendation]
67
+ execution: ExecutionResult | None = None
68
+ task_profile: TaskProfile
69
+ meta: RouteMeta
70
+
71
+
72
+ class Agent(Model):
73
+ id: str
74
+ name: str
75
+ description: str
76
+ provider: str
77
+ tags: list[str]
78
+ combined_score: float
79
+ pricing_model: str
80
+ status: str
81
+
82
+
83
+ class Pagination(Model):
84
+ page: int
85
+ limit: int
86
+ total: int
87
+ pages: int
88
+
89
+
90
+ class AgentListResponse(Model):
91
+ agents: list[Agent]
92
+ pagination: Pagination
93
+
94
+
95
+ class FeedbackResponse(Model):
96
+ accepted: bool
97
+ agent_id: str
98
+ new_combined_score: float
99
+ sample_count: int
100
+
101
+
102
+ class UsageResponse(Model):
103
+ tier: str
104
+ requests_this_month: int
105
+ monthly_limit: int
106
+ estimated_bill_usd: float
File without changes
@@ -0,0 +1,68 @@
1
+ import asyncio
2
+
3
+ import httpx
4
+ import pytest
5
+
6
+ from switchlane import AsyncSwitchlane, Switchlane, SwitchlaneError
7
+
8
+
9
+ ROUTE_RESPONSE = {
10
+ "recommendations": [],
11
+ "task_profile": {
12
+ "category": "unknown",
13
+ "subcategory": None,
14
+ "language": None,
15
+ "input_type": None,
16
+ "output_type": None,
17
+ "complexity": "medium",
18
+ "keywords": [],
19
+ },
20
+ "meta": {
21
+ "match_path": "llm_intent",
22
+ "candidates_evaluated": 0,
23
+ "elapsed_ms": 5,
24
+ "abstained": True,
25
+ "abstention_reason": "no_candidates",
26
+ "confidence": None,
27
+ },
28
+ }
29
+
30
+
31
+ def test_sync_route_parses_abstention_and_sends_auth() -> None:
32
+ def handler(request: httpx.Request) -> httpx.Response:
33
+ assert request.headers["Authorization"] == "Bearer sl_live_test"
34
+ assert request.url.path == "/v1/route"
35
+ return httpx.Response(200, json=ROUTE_RESPONSE)
36
+
37
+ with Switchlane("sl_live_test", base_url="https://example.test", transport=httpx.MockTransport(handler)) as client:
38
+ result = client.route("unknown task")
39
+
40
+ assert result.meta.abstained is True
41
+ assert result.meta.abstention_reason == "no_candidates"
42
+
43
+
44
+ def test_sync_errors_include_status_and_body() -> None:
45
+ transport = httpx.MockTransport(lambda _: httpx.Response(401, json={"error": "Invalid API key"}))
46
+ with Switchlane("bad", base_url="https://example.test", transport=transport) as client:
47
+ with pytest.raises(SwitchlaneError) as caught:
48
+ client.usage()
49
+
50
+ assert caught.value.status_code == 401
51
+ assert caught.value.body == {"error": "Invalid API key"}
52
+
53
+
54
+ def test_async_route() -> None:
55
+ async def run() -> None:
56
+ async def handler(request: httpx.Request) -> httpx.Response:
57
+ assert request.url.path == "/v1/route"
58
+ return httpx.Response(200, json=ROUTE_RESPONSE)
59
+
60
+ async with AsyncSwitchlane(
61
+ "sl_live_test",
62
+ base_url="https://example.test",
63
+ transport=httpx.MockTransport(handler),
64
+ ) as client:
65
+ result = await client.route("unknown task")
66
+ assert result.meta.confidence is None
67
+
68
+ asyncio.run(run())