pyagent-providers 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.
- pyagent_providers-0.1.0/.gitignore +18 -0
- pyagent_providers-0.1.0/PKG-INFO +347 -0
- pyagent_providers-0.1.0/README.md +309 -0
- pyagent_providers-0.1.0/pyproject.toml +39 -0
- pyagent_providers-0.1.0/src/pyagent_providers/__init__.py +30 -0
- pyagent_providers-0.1.0/src/pyagent_providers/adapters/__init__.py +27 -0
- pyagent_providers-0.1.0/src/pyagent_providers/adapters/anthropic.py +106 -0
- pyagent_providers-0.1.0/src/pyagent_providers/adapters/litellm.py +90 -0
- pyagent_providers-0.1.0/src/pyagent_providers/adapters/mock.py +72 -0
- pyagent_providers-0.1.0/src/pyagent_providers/adapters/openai.py +90 -0
- pyagent_providers-0.1.0/src/pyagent_providers/base.py +95 -0
- pyagent_providers-0.1.0/src/pyagent_providers/cost.py +113 -0
- pyagent_providers-0.1.0/src/pyagent_providers/fallback.py +126 -0
- pyagent_providers-0.1.0/src/pyagent_providers/negotiation.py +180 -0
- pyagent_providers-0.1.0/src/pyagent_providers/py.typed +0 -0
- pyagent_providers-0.1.0/src/pyagent_providers/registry.py +166 -0
- pyagent_providers-0.1.0/src/pyagent_providers/router.py +182 -0
- pyagent_providers-0.1.0/tests/__init__.py +0 -0
- pyagent_providers-0.1.0/tests/test_fallback.py +115 -0
- pyagent_providers-0.1.0/tests/test_negotiation.py +115 -0
- pyagent_providers-0.1.0/tests/test_registry.py +140 -0
- pyagent_providers-0.1.0/tests/test_router.py +103 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyagent-providers
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-provider abstraction with capability negotiation, health checks, and fallback chains
|
|
5
|
+
Project-URL: Homepage, https://pyagent.org
|
|
6
|
+
Project-URL: Repository, https://github.com/pyagent-core/pyagent
|
|
7
|
+
Project-URL: Documentation, https://pyagent.org
|
|
8
|
+
Author-email: PyAgent Team <team@pyagent.org>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: LLM,agents,fallback,multi-provider,providers,routing
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: pyagent-patterns>=0.1.0
|
|
21
|
+
Requires-Dist: pyagent-router>=0.1.0
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Requires-Dist: anthropic>=0.30; extra == 'all'
|
|
24
|
+
Requires-Dist: litellm>=1.40; extra == 'all'
|
|
25
|
+
Requires-Dist: openai>=1.30; extra == 'all'
|
|
26
|
+
Provides-Extra: anthropic
|
|
27
|
+
Requires-Dist: anthropic>=0.30; extra == 'anthropic'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
33
|
+
Provides-Extra: litellm
|
|
34
|
+
Requires-Dist: litellm>=1.40; extra == 'litellm'
|
|
35
|
+
Provides-Extra: openai
|
|
36
|
+
Requires-Dist: openai>=1.30; extra == 'openai'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# pyagent-providers
|
|
40
|
+
|
|
41
|
+
**Multi-provider abstraction with capability negotiation, health checks, fallback chains, and cost-optimized routing** for multi-agent LLM systems. Drop-in replacement for hardcoded model specs.
|
|
42
|
+
|
|
43
|
+
[](../../LICENSE)
|
|
44
|
+
[](https://www.python.org/downloads/)
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install pyagent-providers # Core (includes MockProvider)
|
|
50
|
+
pip install pyagent-providers[openai] # + OpenAI adapter
|
|
51
|
+
pip install pyagent-providers[anthropic] # + Anthropic adapter
|
|
52
|
+
pip install pyagent-providers[litellm] # + LiteLLM (100+ models)
|
|
53
|
+
pip install pyagent-providers[all] # All adapters
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Depends on: `pyagent-patterns`, `pyagent-router`.
|
|
57
|
+
|
|
58
|
+
## Why Provider Abstraction?
|
|
59
|
+
|
|
60
|
+
Without `pyagent-providers`, switching models means rewriting LLM wrappers. With it, your agents talk to a `ProviderProtocol` that satisfies the existing `LLMCallable` interface — so every provider is a drop-in replacement for `Agent.llm`.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pyagent_patterns.base import Agent
|
|
64
|
+
from pyagent_providers.adapters.mock import MockProvider
|
|
65
|
+
|
|
66
|
+
# Any provider works as an Agent's LLM
|
|
67
|
+
provider = MockProvider(name="test", responses=["Hello"])
|
|
68
|
+
agent = Agent("greeter", provider) # provider satisfies LLMCallable
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## ProviderProtocol — The Interface
|
|
72
|
+
|
|
73
|
+
Every provider implements this:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from pyagent_providers import ProviderProtocol, HealthStatus, ProviderCapabilities
|
|
77
|
+
|
|
78
|
+
class MyCustomProvider:
|
|
79
|
+
@property
|
|
80
|
+
def name(self) -> str:
|
|
81
|
+
return "my_provider"
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def capabilities(self) -> ProviderCapabilities:
|
|
85
|
+
return ProviderCapabilities(
|
|
86
|
+
models=["my-model-small", "my-model-large"],
|
|
87
|
+
capabilities={Capability.GENERAL, Capability.CODE},
|
|
88
|
+
max_context=128_000,
|
|
89
|
+
supports_streaming=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def health(self) -> HealthStatus:
|
|
93
|
+
# check your endpoint
|
|
94
|
+
return HealthStatus.HEALTHY
|
|
95
|
+
|
|
96
|
+
async def complete(self, messages, model=None) -> str:
|
|
97
|
+
# call your API
|
|
98
|
+
return "response"
|
|
99
|
+
|
|
100
|
+
async def __call__(self, messages) -> str:
|
|
101
|
+
return await self.complete(messages)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## ProviderRegistry — Register and Discover
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import asyncio
|
|
108
|
+
from pyagent_providers import ProviderRegistry, HealthStatus
|
|
109
|
+
from pyagent_providers.adapters.mock import MockProvider
|
|
110
|
+
from pyagent_router.selector import Capability
|
|
111
|
+
|
|
112
|
+
registry = ProviderRegistry()
|
|
113
|
+
|
|
114
|
+
async def setup():
|
|
115
|
+
await registry.register(MockProvider(
|
|
116
|
+
name="openai",
|
|
117
|
+
models=["gpt-4o-mini", "gpt-4o"],
|
|
118
|
+
capabilities={Capability.GENERAL, Capability.CODE, Capability.VISION},
|
|
119
|
+
))
|
|
120
|
+
await registry.register(MockProvider(
|
|
121
|
+
name="anthropic",
|
|
122
|
+
models=["claude-haiku-3.5", "claude-sonnet-4"],
|
|
123
|
+
capabilities={Capability.GENERAL, Capability.CODE, Capability.CREATIVE},
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
# Discover by capability
|
|
127
|
+
coders = registry.discover({Capability.CODE})
|
|
128
|
+
print([p.name for p in coders]) # ["openai", "anthropic"]
|
|
129
|
+
|
|
130
|
+
vision = registry.discover({Capability.VISION})
|
|
131
|
+
print([p.name for p in vision]) # ["openai"]
|
|
132
|
+
|
|
133
|
+
# Health check all
|
|
134
|
+
statuses = await registry.check_health()
|
|
135
|
+
print(statuses) # {"openai": "healthy", "anthropic": "healthy"}
|
|
136
|
+
|
|
137
|
+
# Remove unhealthy
|
|
138
|
+
removed = await registry.remove_unhealthy()
|
|
139
|
+
print(f"Removed: {removed}") # []
|
|
140
|
+
|
|
141
|
+
asyncio.run(setup())
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## ProviderRouter — Strategy-Based Routing
|
|
145
|
+
|
|
146
|
+
Four strategies: `CAPABILITY_FIRST`, `COST_FIRST`, `LATENCY_FIRST`, `ROUND_ROBIN`.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from pyagent_providers import ProviderRouter, RoutingStrategy
|
|
150
|
+
from pyagent_patterns.base import Message
|
|
151
|
+
|
|
152
|
+
# Capability-first (default): pick the provider with broadest capabilities
|
|
153
|
+
router = ProviderRouter(registry, strategy=RoutingStrategy.CAPABILITY_FIRST)
|
|
154
|
+
provider, model = asyncio.run(router.route(
|
|
155
|
+
[Message.user("Write a Python REST API with FastAPI")],
|
|
156
|
+
required={Capability.CODE},
|
|
157
|
+
))
|
|
158
|
+
print(f"{provider.name}/{model}")
|
|
159
|
+
|
|
160
|
+
# Cost-first: cheapest provider + model for the task
|
|
161
|
+
router = ProviderRouter(registry, strategy=RoutingStrategy.COST_FIRST)
|
|
162
|
+
provider, model = asyncio.run(router.route([Message.user("What is 2+2?")]))
|
|
163
|
+
print(f"{provider.name}/{model}") # picks gpt-4.1-nano
|
|
164
|
+
|
|
165
|
+
# Round-robin: cycle through providers for load distribution
|
|
166
|
+
router = ProviderRouter(registry, strategy=RoutingStrategy.ROUND_ROBIN)
|
|
167
|
+
for _ in range(4):
|
|
168
|
+
provider, model = asyncio.run(router.route([Message.user("Balance me")]))
|
|
169
|
+
print(provider.name, end=" ")
|
|
170
|
+
# openai anthropic openai anthropic
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## FallbackChain — Resilient Completion
|
|
174
|
+
|
|
175
|
+
Try providers in order. If one fails, fall through to the next. Optionally integrates with `CircuitBreaker` from `pyagent-patterns`.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from pyagent_providers import FallbackChain
|
|
179
|
+
|
|
180
|
+
chain = FallbackChain(providers=[
|
|
181
|
+
primary_openai, # try first
|
|
182
|
+
fallback_anthropic, # if OpenAI fails
|
|
183
|
+
emergency_litellm, # last resort
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
result = asyncio.run(chain.complete([Message.user("Important task")]))
|
|
187
|
+
print(result.output) # response from first successful provider
|
|
188
|
+
print(result.provider_name) # which provider answered
|
|
189
|
+
print(result.attempts) # full attempt log with errors
|
|
190
|
+
|
|
191
|
+
# With circuit breaker integration
|
|
192
|
+
from pyagent_patterns.recovery import CircuitBreaker
|
|
193
|
+
|
|
194
|
+
chain = FallbackChain(
|
|
195
|
+
providers=[primary, fallback],
|
|
196
|
+
circuit_breakers={
|
|
197
|
+
"primary": CircuitBreaker(failure_threshold=3, reset_timeout_seconds=60),
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## CapabilityNegotiator — Match Task Requirements
|
|
203
|
+
|
|
204
|
+
Scores providers by capability overlap, context window, and feature support.
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from pyagent_providers import CapabilityNegotiator
|
|
208
|
+
|
|
209
|
+
negotiator = CapabilityNegotiator(registry)
|
|
210
|
+
|
|
211
|
+
# Find best provider for code + reasoning tasks
|
|
212
|
+
result = negotiator.negotiate(
|
|
213
|
+
required_capabilities={Capability.CODE, Capability.REASONING},
|
|
214
|
+
min_context=100_000,
|
|
215
|
+
)
|
|
216
|
+
if result:
|
|
217
|
+
print(result.provider.name) # "openai" or "anthropic"
|
|
218
|
+
print(result.model) # best model from that provider
|
|
219
|
+
print(f"Match: {result.match_score:.0%}")
|
|
220
|
+
print(result.matched_capabilities) # {CODE, REASONING}
|
|
221
|
+
print(result.missing_capabilities) # set()
|
|
222
|
+
|
|
223
|
+
# Get all ranked matches
|
|
224
|
+
all_matches = negotiator.negotiate_all(
|
|
225
|
+
required_capabilities={Capability.GENERAL},
|
|
226
|
+
limit=5,
|
|
227
|
+
)
|
|
228
|
+
for m in all_matches:
|
|
229
|
+
print(f" {m.provider.name}: {m.match_score:.0%}")
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## CostOptimizer — Multi-Provider Cost Comparison
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from pyagent_providers import CostOptimizer
|
|
236
|
+
|
|
237
|
+
optimizer = CostOptimizer(registry)
|
|
238
|
+
|
|
239
|
+
# Compare all providers for a task
|
|
240
|
+
estimates = optimizer.compare("Explain distributed consensus algorithms")
|
|
241
|
+
for est in estimates[:5]:
|
|
242
|
+
print(f"{est.provider_name}/{est.model}: ${est.estimate.total_cost:.7f}")
|
|
243
|
+
|
|
244
|
+
# Get cheapest option
|
|
245
|
+
cheapest = optimizer.cheapest("Simple greeting task")
|
|
246
|
+
if cheapest:
|
|
247
|
+
print(f"Use {cheapest.provider_name}/{cheapest.model}: ${cheapest.estimate.total_cost:.7f}")
|
|
248
|
+
|
|
249
|
+
# Get provider object + model for direct use
|
|
250
|
+
pair = optimizer.cheapest_provider("My task")
|
|
251
|
+
if pair:
|
|
252
|
+
provider, model = pair
|
|
253
|
+
agent = Agent("my_agent", provider)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Adapter Examples
|
|
257
|
+
|
|
258
|
+
### OpenAI
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from pyagent_providers.adapters.openai import OpenAIProvider
|
|
262
|
+
|
|
263
|
+
openai = OpenAIProvider(
|
|
264
|
+
api_key="sk-...", # or set OPENAI_API_KEY env var
|
|
265
|
+
default_model="gpt-4o-mini",
|
|
266
|
+
models=["gpt-4o-mini", "gpt-4o", "o3-mini"],
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
await registry.register(openai)
|
|
270
|
+
result = await openai.complete([Message.user("Hello")])
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Anthropic
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from pyagent_providers.adapters.anthropic import AnthropicProvider
|
|
277
|
+
|
|
278
|
+
anthropic = AnthropicProvider(
|
|
279
|
+
api_key="sk-ant-...",
|
|
280
|
+
default_model="claude-sonnet-4-20250514",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
await registry.register(anthropic)
|
|
284
|
+
result = await anthropic.complete([Message.user("Hello")])
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### LiteLLM (100+ Providers)
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from pyagent_providers.adapters.litellm import LiteLLMProvider
|
|
291
|
+
|
|
292
|
+
litellm = LiteLLMProvider(
|
|
293
|
+
models=["gpt-4o-mini", "anthropic/claude-haiku-3.5", "gemini/gemini-2.5-flash"],
|
|
294
|
+
default_model="gpt-4o-mini",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
await registry.register(litellm)
|
|
298
|
+
result = await litellm.complete([Message.user("Hello")], model="gemini/gemini-2.5-flash")
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### MockProvider (Testing)
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from pyagent_providers.adapters.mock import MockProvider
|
|
305
|
+
|
|
306
|
+
mock = MockProvider(
|
|
307
|
+
name="test",
|
|
308
|
+
responses=["Response 1", "Response 2"],
|
|
309
|
+
models=["mock-fast", "mock-smart"],
|
|
310
|
+
capabilities={Capability.GENERAL, Capability.CODE},
|
|
311
|
+
health_status=HealthStatus.HEALTHY,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
await registry.register(mock)
|
|
315
|
+
result = await mock.complete([Message.user("Test")])
|
|
316
|
+
print(mock.call_count) # 1
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Integration with pyagent-patterns
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
from pyagent_patterns.base import Agent
|
|
323
|
+
from pyagent_patterns.orchestration import Pipeline
|
|
324
|
+
from pyagent_providers import ProviderRegistry, CapabilityNegotiator
|
|
325
|
+
from pyagent_providers.adapters.mock import MockProvider
|
|
326
|
+
|
|
327
|
+
# Set up providers
|
|
328
|
+
registry = ProviderRegistry()
|
|
329
|
+
registry.register_sync(MockProvider(name="fast", responses=["Extracted facts"]))
|
|
330
|
+
registry.register_sync(MockProvider(name="smart", responses=["Detailed analysis"]))
|
|
331
|
+
|
|
332
|
+
# Use providers as Agent LLMs
|
|
333
|
+
fast = registry.get("fast")
|
|
334
|
+
smart = registry.get("smart")
|
|
335
|
+
|
|
336
|
+
pipeline = Pipeline(stages=[
|
|
337
|
+
Agent("extractor", fast, system_prompt="Extract key facts."),
|
|
338
|
+
Agent("analyst", smart, system_prompt="Analyse in depth."),
|
|
339
|
+
])
|
|
340
|
+
|
|
341
|
+
result = asyncio.run(pipeline.run("Process this document"))
|
|
342
|
+
print(result.output)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Full Documentation
|
|
346
|
+
|
|
347
|
+
See [pyagent.dev](https://pyagent.dev) for full API reference and integration guides.
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# pyagent-providers
|
|
2
|
+
|
|
3
|
+
**Multi-provider abstraction with capability negotiation, health checks, fallback chains, and cost-optimized routing** for multi-agent LLM systems. Drop-in replacement for hardcoded model specs.
|
|
4
|
+
|
|
5
|
+
[](../../LICENSE)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install pyagent-providers # Core (includes MockProvider)
|
|
12
|
+
pip install pyagent-providers[openai] # + OpenAI adapter
|
|
13
|
+
pip install pyagent-providers[anthropic] # + Anthropic adapter
|
|
14
|
+
pip install pyagent-providers[litellm] # + LiteLLM (100+ models)
|
|
15
|
+
pip install pyagent-providers[all] # All adapters
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Depends on: `pyagent-patterns`, `pyagent-router`.
|
|
19
|
+
|
|
20
|
+
## Why Provider Abstraction?
|
|
21
|
+
|
|
22
|
+
Without `pyagent-providers`, switching models means rewriting LLM wrappers. With it, your agents talk to a `ProviderProtocol` that satisfies the existing `LLMCallable` interface — so every provider is a drop-in replacement for `Agent.llm`.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pyagent_patterns.base import Agent
|
|
26
|
+
from pyagent_providers.adapters.mock import MockProvider
|
|
27
|
+
|
|
28
|
+
# Any provider works as an Agent's LLM
|
|
29
|
+
provider = MockProvider(name="test", responses=["Hello"])
|
|
30
|
+
agent = Agent("greeter", provider) # provider satisfies LLMCallable
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## ProviderProtocol — The Interface
|
|
34
|
+
|
|
35
|
+
Every provider implements this:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from pyagent_providers import ProviderProtocol, HealthStatus, ProviderCapabilities
|
|
39
|
+
|
|
40
|
+
class MyCustomProvider:
|
|
41
|
+
@property
|
|
42
|
+
def name(self) -> str:
|
|
43
|
+
return "my_provider"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def capabilities(self) -> ProviderCapabilities:
|
|
47
|
+
return ProviderCapabilities(
|
|
48
|
+
models=["my-model-small", "my-model-large"],
|
|
49
|
+
capabilities={Capability.GENERAL, Capability.CODE},
|
|
50
|
+
max_context=128_000,
|
|
51
|
+
supports_streaming=True,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
async def health(self) -> HealthStatus:
|
|
55
|
+
# check your endpoint
|
|
56
|
+
return HealthStatus.HEALTHY
|
|
57
|
+
|
|
58
|
+
async def complete(self, messages, model=None) -> str:
|
|
59
|
+
# call your API
|
|
60
|
+
return "response"
|
|
61
|
+
|
|
62
|
+
async def __call__(self, messages) -> str:
|
|
63
|
+
return await self.complete(messages)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## ProviderRegistry — Register and Discover
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from pyagent_providers import ProviderRegistry, HealthStatus
|
|
71
|
+
from pyagent_providers.adapters.mock import MockProvider
|
|
72
|
+
from pyagent_router.selector import Capability
|
|
73
|
+
|
|
74
|
+
registry = ProviderRegistry()
|
|
75
|
+
|
|
76
|
+
async def setup():
|
|
77
|
+
await registry.register(MockProvider(
|
|
78
|
+
name="openai",
|
|
79
|
+
models=["gpt-4o-mini", "gpt-4o"],
|
|
80
|
+
capabilities={Capability.GENERAL, Capability.CODE, Capability.VISION},
|
|
81
|
+
))
|
|
82
|
+
await registry.register(MockProvider(
|
|
83
|
+
name="anthropic",
|
|
84
|
+
models=["claude-haiku-3.5", "claude-sonnet-4"],
|
|
85
|
+
capabilities={Capability.GENERAL, Capability.CODE, Capability.CREATIVE},
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
# Discover by capability
|
|
89
|
+
coders = registry.discover({Capability.CODE})
|
|
90
|
+
print([p.name for p in coders]) # ["openai", "anthropic"]
|
|
91
|
+
|
|
92
|
+
vision = registry.discover({Capability.VISION})
|
|
93
|
+
print([p.name for p in vision]) # ["openai"]
|
|
94
|
+
|
|
95
|
+
# Health check all
|
|
96
|
+
statuses = await registry.check_health()
|
|
97
|
+
print(statuses) # {"openai": "healthy", "anthropic": "healthy"}
|
|
98
|
+
|
|
99
|
+
# Remove unhealthy
|
|
100
|
+
removed = await registry.remove_unhealthy()
|
|
101
|
+
print(f"Removed: {removed}") # []
|
|
102
|
+
|
|
103
|
+
asyncio.run(setup())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## ProviderRouter — Strategy-Based Routing
|
|
107
|
+
|
|
108
|
+
Four strategies: `CAPABILITY_FIRST`, `COST_FIRST`, `LATENCY_FIRST`, `ROUND_ROBIN`.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from pyagent_providers import ProviderRouter, RoutingStrategy
|
|
112
|
+
from pyagent_patterns.base import Message
|
|
113
|
+
|
|
114
|
+
# Capability-first (default): pick the provider with broadest capabilities
|
|
115
|
+
router = ProviderRouter(registry, strategy=RoutingStrategy.CAPABILITY_FIRST)
|
|
116
|
+
provider, model = asyncio.run(router.route(
|
|
117
|
+
[Message.user("Write a Python REST API with FastAPI")],
|
|
118
|
+
required={Capability.CODE},
|
|
119
|
+
))
|
|
120
|
+
print(f"{provider.name}/{model}")
|
|
121
|
+
|
|
122
|
+
# Cost-first: cheapest provider + model for the task
|
|
123
|
+
router = ProviderRouter(registry, strategy=RoutingStrategy.COST_FIRST)
|
|
124
|
+
provider, model = asyncio.run(router.route([Message.user("What is 2+2?")]))
|
|
125
|
+
print(f"{provider.name}/{model}") # picks gpt-4.1-nano
|
|
126
|
+
|
|
127
|
+
# Round-robin: cycle through providers for load distribution
|
|
128
|
+
router = ProviderRouter(registry, strategy=RoutingStrategy.ROUND_ROBIN)
|
|
129
|
+
for _ in range(4):
|
|
130
|
+
provider, model = asyncio.run(router.route([Message.user("Balance me")]))
|
|
131
|
+
print(provider.name, end=" ")
|
|
132
|
+
# openai anthropic openai anthropic
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## FallbackChain — Resilient Completion
|
|
136
|
+
|
|
137
|
+
Try providers in order. If one fails, fall through to the next. Optionally integrates with `CircuitBreaker` from `pyagent-patterns`.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from pyagent_providers import FallbackChain
|
|
141
|
+
|
|
142
|
+
chain = FallbackChain(providers=[
|
|
143
|
+
primary_openai, # try first
|
|
144
|
+
fallback_anthropic, # if OpenAI fails
|
|
145
|
+
emergency_litellm, # last resort
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
result = asyncio.run(chain.complete([Message.user("Important task")]))
|
|
149
|
+
print(result.output) # response from first successful provider
|
|
150
|
+
print(result.provider_name) # which provider answered
|
|
151
|
+
print(result.attempts) # full attempt log with errors
|
|
152
|
+
|
|
153
|
+
# With circuit breaker integration
|
|
154
|
+
from pyagent_patterns.recovery import CircuitBreaker
|
|
155
|
+
|
|
156
|
+
chain = FallbackChain(
|
|
157
|
+
providers=[primary, fallback],
|
|
158
|
+
circuit_breakers={
|
|
159
|
+
"primary": CircuitBreaker(failure_threshold=3, reset_timeout_seconds=60),
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## CapabilityNegotiator — Match Task Requirements
|
|
165
|
+
|
|
166
|
+
Scores providers by capability overlap, context window, and feature support.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from pyagent_providers import CapabilityNegotiator
|
|
170
|
+
|
|
171
|
+
negotiator = CapabilityNegotiator(registry)
|
|
172
|
+
|
|
173
|
+
# Find best provider for code + reasoning tasks
|
|
174
|
+
result = negotiator.negotiate(
|
|
175
|
+
required_capabilities={Capability.CODE, Capability.REASONING},
|
|
176
|
+
min_context=100_000,
|
|
177
|
+
)
|
|
178
|
+
if result:
|
|
179
|
+
print(result.provider.name) # "openai" or "anthropic"
|
|
180
|
+
print(result.model) # best model from that provider
|
|
181
|
+
print(f"Match: {result.match_score:.0%}")
|
|
182
|
+
print(result.matched_capabilities) # {CODE, REASONING}
|
|
183
|
+
print(result.missing_capabilities) # set()
|
|
184
|
+
|
|
185
|
+
# Get all ranked matches
|
|
186
|
+
all_matches = negotiator.negotiate_all(
|
|
187
|
+
required_capabilities={Capability.GENERAL},
|
|
188
|
+
limit=5,
|
|
189
|
+
)
|
|
190
|
+
for m in all_matches:
|
|
191
|
+
print(f" {m.provider.name}: {m.match_score:.0%}")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## CostOptimizer — Multi-Provider Cost Comparison
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from pyagent_providers import CostOptimizer
|
|
198
|
+
|
|
199
|
+
optimizer = CostOptimizer(registry)
|
|
200
|
+
|
|
201
|
+
# Compare all providers for a task
|
|
202
|
+
estimates = optimizer.compare("Explain distributed consensus algorithms")
|
|
203
|
+
for est in estimates[:5]:
|
|
204
|
+
print(f"{est.provider_name}/{est.model}: ${est.estimate.total_cost:.7f}")
|
|
205
|
+
|
|
206
|
+
# Get cheapest option
|
|
207
|
+
cheapest = optimizer.cheapest("Simple greeting task")
|
|
208
|
+
if cheapest:
|
|
209
|
+
print(f"Use {cheapest.provider_name}/{cheapest.model}: ${cheapest.estimate.total_cost:.7f}")
|
|
210
|
+
|
|
211
|
+
# Get provider object + model for direct use
|
|
212
|
+
pair = optimizer.cheapest_provider("My task")
|
|
213
|
+
if pair:
|
|
214
|
+
provider, model = pair
|
|
215
|
+
agent = Agent("my_agent", provider)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Adapter Examples
|
|
219
|
+
|
|
220
|
+
### OpenAI
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from pyagent_providers.adapters.openai import OpenAIProvider
|
|
224
|
+
|
|
225
|
+
openai = OpenAIProvider(
|
|
226
|
+
api_key="sk-...", # or set OPENAI_API_KEY env var
|
|
227
|
+
default_model="gpt-4o-mini",
|
|
228
|
+
models=["gpt-4o-mini", "gpt-4o", "o3-mini"],
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
await registry.register(openai)
|
|
232
|
+
result = await openai.complete([Message.user("Hello")])
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Anthropic
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from pyagent_providers.adapters.anthropic import AnthropicProvider
|
|
239
|
+
|
|
240
|
+
anthropic = AnthropicProvider(
|
|
241
|
+
api_key="sk-ant-...",
|
|
242
|
+
default_model="claude-sonnet-4-20250514",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
await registry.register(anthropic)
|
|
246
|
+
result = await anthropic.complete([Message.user("Hello")])
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### LiteLLM (100+ Providers)
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from pyagent_providers.adapters.litellm import LiteLLMProvider
|
|
253
|
+
|
|
254
|
+
litellm = LiteLLMProvider(
|
|
255
|
+
models=["gpt-4o-mini", "anthropic/claude-haiku-3.5", "gemini/gemini-2.5-flash"],
|
|
256
|
+
default_model="gpt-4o-mini",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
await registry.register(litellm)
|
|
260
|
+
result = await litellm.complete([Message.user("Hello")], model="gemini/gemini-2.5-flash")
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### MockProvider (Testing)
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
from pyagent_providers.adapters.mock import MockProvider
|
|
267
|
+
|
|
268
|
+
mock = MockProvider(
|
|
269
|
+
name="test",
|
|
270
|
+
responses=["Response 1", "Response 2"],
|
|
271
|
+
models=["mock-fast", "mock-smart"],
|
|
272
|
+
capabilities={Capability.GENERAL, Capability.CODE},
|
|
273
|
+
health_status=HealthStatus.HEALTHY,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
await registry.register(mock)
|
|
277
|
+
result = await mock.complete([Message.user("Test")])
|
|
278
|
+
print(mock.call_count) # 1
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Integration with pyagent-patterns
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from pyagent_patterns.base import Agent
|
|
285
|
+
from pyagent_patterns.orchestration import Pipeline
|
|
286
|
+
from pyagent_providers import ProviderRegistry, CapabilityNegotiator
|
|
287
|
+
from pyagent_providers.adapters.mock import MockProvider
|
|
288
|
+
|
|
289
|
+
# Set up providers
|
|
290
|
+
registry = ProviderRegistry()
|
|
291
|
+
registry.register_sync(MockProvider(name="fast", responses=["Extracted facts"]))
|
|
292
|
+
registry.register_sync(MockProvider(name="smart", responses=["Detailed analysis"]))
|
|
293
|
+
|
|
294
|
+
# Use providers as Agent LLMs
|
|
295
|
+
fast = registry.get("fast")
|
|
296
|
+
smart = registry.get("smart")
|
|
297
|
+
|
|
298
|
+
pipeline = Pipeline(stages=[
|
|
299
|
+
Agent("extractor", fast, system_prompt="Extract key facts."),
|
|
300
|
+
Agent("analyst", smart, system_prompt="Analyse in depth."),
|
|
301
|
+
])
|
|
302
|
+
|
|
303
|
+
result = asyncio.run(pipeline.run("Process this document"))
|
|
304
|
+
print(result.output)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Full Documentation
|
|
308
|
+
|
|
309
|
+
See [pyagent.dev](https://pyagent.dev) for full API reference and integration guides.
|