adaptive-memory-multi-model-router 2.16.0 → 2.16.2
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.
- package/.github/workflows/adapters-ci.yml +142 -0
- package/.github/workflows/auto-submit-sitemap.yml +41 -0
- package/.github/workflows/ci.yml +2 -5
- package/.github/workflows/mcp-pypi-publish.yml +34 -0
- package/.github/workflows/pypi-publish.yml +146 -0
- package/.github/workflows/tmlpd-publish.yml +23 -0
- package/README.md +245 -148
- package/RELEASE_v2.16.0.md +149 -0
- package/TECHNICAL_README.md +253 -0
- package/adapters/README.md +36 -0
- package/adapters/__init__.py +25 -0
- package/adapters/a3m_adapter/__init__.py +51 -0
- package/adapters/a3m_adapter/adapter/__init__.py +22 -0
- package/adapters/a3m_adapter/adapter/autogen.py +169 -0
- package/adapters/a3m_adapter/adapter/config.py +100 -0
- package/adapters/a3m_adapter/adapter/haystack.py +197 -0
- package/adapters/a3m_adapter/adapter/langchain.py +155 -0
- package/adapters/a3m_adapter/adapter/langgraph.py +196 -0
- package/adapters/a3m_adapter/adapter/llamaindex.py +162 -0
- package/adapters/a3m_adapter/adapter/pinecone.py +217 -0
- package/adapters/a3m_adapter/adapter/vercel.py +188 -0
- package/adapters/a3m_adapter/tests/__init__.py +1 -0
- package/adapters/a3m_adapter/tests/test_adapters.py +118 -0
- package/adapters/a3m_adapter/tests/test_integration.py +80 -0
- package/adapters/requirements-dev.txt +6 -0
- package/adapters/requirements.txt +4 -0
- package/adapters/setup.py +23 -0
- package/demo.py +251 -0
- package/discoverability-diagnosis.md +280 -0
- package/dist/analytics/costAnalytics.d.ts.map +1 -1
- package/dist/benchmark/reproducible.d.ts.map +1 -1
- package/dist/cache/semanticCache.d.ts.map +1 -1
- package/dist/cli/setupWizard.d.ts +257 -50
- package/dist/cli/setupWizard.d.ts.map +1 -1
- package/dist/cli/setupWizard.js +419 -109
- package/dist/cli/setupWizard.js.map +1 -1
- package/dist/cli/tui.d.ts +6 -0
- package/dist/cli/tui.js +96 -67
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli.js +9 -0
- package/dist/cost/budgetEnforcer.d.ts.map +1 -1
- package/dist/cost/costTracker.d.ts.map +1 -1
- package/dist/ensemble/multiRoundDialog.d.ts.map +1 -1
- package/dist/ensemble/shapleyValue.d.ts.map +1 -1
- package/dist/ensemble.d.ts +1 -1
- package/dist/ensemble.js +141 -0
- package/dist/integrations/langchainAdapter.d.ts.map +1 -1
- package/dist/integrations/langchainAdapter.js +3 -3
- package/dist/integrations/langchainAdapter.js.map +1 -1
- package/dist/integrations/oauth.d.ts.map +1 -1
- package/dist/integrations/scienceAdapter.d.ts.map +1 -1
- package/dist/memory/autoFetch.d.ts.map +1 -1
- package/dist/memory/hybridMemory.d.ts.map +1 -1
- package/dist/memory/memoryTree.d.ts.map +1 -1
- package/dist/memory/obsidianVault.d.ts.map +1 -1
- package/dist/memory/reasoningBank.d.ts.map +1 -1
- package/dist/observability/metrics.d.ts.map +1 -1
- package/dist/observability/tracer.d.ts.map +1 -1
- package/dist/providers/providerConfig.d.ts.map +1 -1
- package/dist/providers/providerConfig.js +32 -17
- package/dist/providers/providerConfig.js.map +1 -1
- package/dist/routing/advancedRouter.d.ts.map +1 -1
- package/dist/routing/advancedRouter.js +106 -14
- package/dist/routing/advancedRouter.js.map +1 -1
- package/dist/routing/providerHealth.d.ts.map +1 -1
- package/dist/routing/providerRetry.d.ts.map +1 -1
- package/dist/routing/shadowSampler.d.ts.map +1 -0
- package/dist/routing/shadowSampler.js.map +1 -1
- package/dist/security/guardrails.d.ts.map +1 -1
- package/dist/server/handlers/chatHandler.d.ts.map +1 -1
- package/dist/server/handlers/completionsHandler.d.ts.map +1 -1
- package/dist/server/handlers/embeddingsHandler.d.ts.map +1 -1
- package/dist/server/handlers/healthHandler.d.ts.map +1 -1
- package/dist/server/handlers/metricsHandler.d.ts.map +1 -1
- package/dist/server/handlers/modelsHandler.d.ts.map +1 -1
- package/dist/server/metrics.d.ts.map +1 -1
- package/dist/server/proxyServer.d.ts.map +1 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/state.d.ts.map +1 -1
- package/dist/skills/__tests__/skill_manager.test.js +5 -265
- package/dist/skills/__tests__/skill_manager.test.js.map +1 -1
- package/dist/utils/tokenUtils.d.ts.map +1 -1
- package/docker-compose.yml +84 -60
- package/docs/ARTICLE_Biology_Inspired_Routing.md +208 -0
- package/docs/ARTICLE_Master.md +78 -0
- package/docs/ARTICLE_Master_CN.md +78 -0
- package/docs/ARTICLE_OpenRouter_Stripe.md +140 -0
- package/docs/DEVPTO_ARTICLE.md +84 -0
- package/docs/HUMAN_STYLE_GUIDE.md +75 -0
- package/docs/IMPRINT_PLAN.md +88 -0
- package/docs/OPENROUTER_ALTERNATIVE.md +184 -0
- package/docs/SOCIAL_CAMPAIGN.md +316 -0
- package/docs/anthropic.html +45 -0
- package/docs/best-llm-routers-2025.html +157 -0
- package/docs/cerebras.html +43 -0
- package/docs/cli-cheatsheet.md +286 -212
- package/docs/comparison.md +2 -2
- package/docs/deepseek.html +44 -0
- package/docs/google.html +47 -0
- package/docs/groq.html +44 -0
- package/docs/llms-full.txt +360 -138
- package/docs/llms.txt +70 -71
- package/docs/mistral.html +43 -0
- package/docs/ollama.html +50 -0
- package/docs/openai.html +57 -0
- package/docs/sitemap.xml +69 -57
- package/docs-site/blog/best-llm-routers-2025.html +157 -0
- package/docs-site/index.html +68 -9
- package/docs-site/providers/anthropic.html +45 -0
- package/docs-site/providers/cerebras.html +43 -0
- package/docs-site/providers/deepseek.html +44 -0
- package/docs-site/providers/google.html +47 -0
- package/docs-site/providers/groq.html +44 -0
- package/docs-site/providers/index.html +41 -0
- package/docs-site/providers/mistral.html +43 -0
- package/docs-site/providers/ollama.html +50 -0
- package/docs-site/providers/openai.html +57 -0
- package/docs-site/sitemap.xml +69 -0
- package/llms.txt +70 -62
- package/package.json +44 -182
- package/packages/agentkit-adapter/LICENSE +21 -0
- package/packages/agentkit-adapter/README.md +126 -0
- package/packages/agentkit-adapter/examples/agentkit-example.ts +139 -0
- package/packages/agentkit-adapter/package.json +57 -0
- package/packages/agentkit-adapter/src/adapter.ts +381 -0
- package/packages/agentkit-adapter/src/index.ts +36 -0
- package/packages/agentkit-adapter/src/types.ts +105 -0
- package/packages/agentkit-adapter/src/util.ts +13 -0
- package/packages/agentkit-adapter/tsconfig.json +22 -0
- package/prometheus.yml +8 -0
- package/python/README.md +35 -81
- package/python/a3m/__init__.py +32 -3
- package/python/a3m/adapters/__init__.py +21 -0
- package/python/a3m/adapters/langchain.py +190 -0
- package/python/a3m/adapters/llamaindex.py +249 -0
- package/python/a3m/adapters/qdrant.py +240 -0
- package/python/a3m/adapters/weaviate.py +263 -0
- package/python/a3m/client.py +5 -0
- package/python/build_verify.sh +32 -0
- package/python/mcp-server/README.md +172 -0
- package/python/mcp-server/a3m_mcp/__init__.py +25 -0
- package/python/mcp-server/a3m_mcp/__main__.py +15 -0
- package/python/mcp-server/a3m_mcp/server.py +205 -0
- package/python/mcp-server/pyproject.toml +24 -0
- package/python/pyproject.toml +60 -6
- package/python/setup.py +3 -28
- package/scripts/submit-sitemap.sh +52 -0
- package/src/__types__/registry.d.ts +14 -0
- package/src/cli/setupWizard.ts +443 -112
- package/src/cli/tui.ts +159 -0
- package/src/ensemble.ts +154 -1
- package/src/integrations/langchainAdapter.ts +2 -2
- package/src/providers/providerConfig.ts +32 -17
- package/src/providers/registry.js +27 -0
- package/src/routing/advancedRouter.ts +99 -14
- package/src/routing/shadowSampler.ts +1 -1
- package/test-install/package.json +12 -0
- package/tests/tsconfig.json +0 -1
- package/tmlpd-pi-extension/README.md +105 -44
- package/tmlpd-pi-extension/docs/demo.svg +33 -0
- package/tmlpd-pi-extension/package.json +35 -106
- package/tmlpd-pi-extension/src/tokenOptimization/contextStratifier.ts +163 -0
- package/tmlpd-pi-extension/src/tokenOptimization/fetchOnceLocal.ts +136 -0
- package/tmlpd-pi-extension/src/tokenOptimization/index.ts +197 -0
- package/tmlpd-pi-extension/src/tokenOptimization/interAgentCompression.ts +157 -0
- package/tmlpd-pi-extension/src/tokenOptimization/schemaContract.ts +101 -0
- package/tmlpd-pi-extension/src/tokenOptimization/semanticCache.ts +248 -0
- package/tmlpd-pi-extension/src/tokenOptimization/tokenAwareFallback.ts +192 -0
- package/tmlpd-pi-extension/test/verify.js +21 -0
- package/tsconfig.build.json +3 -2
- package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts +0 -12
- package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts.map +0 -1
- package/packages/a3m-vercel-ai/dist/a3m-language-model.js +0 -289
- package/packages/a3m-vercel-ai/dist/a3m-language-model.js.map +0 -1
- package/packages/a3m-vercel-ai/dist/index.d.ts +0 -82
- package/packages/a3m-vercel-ai/dist/index.d.ts.map +0 -1
- package/packages/a3m-vercel-ai/dist/index.js +0 -79
- package/packages/a3m-vercel-ai/dist/index.js.map +0 -1
- package/packages/a3m-vercel-ai/dist/types.d.ts +0 -97
- package/packages/a3m-vercel-ai/dist/types.d.ts.map +0 -1
- package/packages/a3m-vercel-ai/dist/types.js +0 -5
- package/packages/a3m-vercel-ai/dist/types.js.map +0 -1
- package/src/skills/__tests__/skill_manager.test.ts +0 -328
- package/tmlpd-pi-extension/dist/cache/prefixCache.d.ts +0 -114
- package/tmlpd-pi-extension/dist/cache/prefixCache.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/cache/prefixCache.js +0 -285
- package/tmlpd-pi-extension/dist/cache/prefixCache.js.map +0 -1
- package/tmlpd-pi-extension/dist/cache/responseCache.d.ts +0 -58
- package/tmlpd-pi-extension/dist/cache/responseCache.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/cache/responseCache.js +0 -153
- package/tmlpd-pi-extension/dist/cache/responseCache.js.map +0 -1
- package/tmlpd-pi-extension/dist/cli.js +0 -59
- package/tmlpd-pi-extension/dist/cost/costTracker.d.ts +0 -95
- package/tmlpd-pi-extension/dist/cost/costTracker.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/cost/costTracker.js +0 -240
- package/tmlpd-pi-extension/dist/cost/costTracker.js.map +0 -1
- package/tmlpd-pi-extension/dist/index.d.ts +0 -723
- package/tmlpd-pi-extension/dist/index.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/index.js +0 -239
- package/tmlpd-pi-extension/dist/index.js.map +0 -1
- package/tmlpd-pi-extension/dist/memory/episodicMemory.d.ts +0 -82
- package/tmlpd-pi-extension/dist/memory/episodicMemory.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/memory/episodicMemory.js +0 -145
- package/tmlpd-pi-extension/dist/memory/episodicMemory.js.map +0 -1
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.d.ts +0 -102
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.js +0 -207
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.js.map +0 -1
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.d.ts +0 -85
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.js +0 -210
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.js.map +0 -1
- package/tmlpd-pi-extension/dist/providers/localProvider.d.ts +0 -102
- package/tmlpd-pi-extension/dist/providers/localProvider.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/providers/localProvider.js +0 -338
- package/tmlpd-pi-extension/dist/providers/localProvider.js.map +0 -1
- package/tmlpd-pi-extension/dist/providers/registry.d.ts +0 -55
- package/tmlpd-pi-extension/dist/providers/registry.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/providers/registry.js +0 -138
- package/tmlpd-pi-extension/dist/providers/registry.js.map +0 -1
- package/tmlpd-pi-extension/dist/routing/advancedRouter.d.ts +0 -68
- package/tmlpd-pi-extension/dist/routing/advancedRouter.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/routing/advancedRouter.js +0 -332
- package/tmlpd-pi-extension/dist/routing/advancedRouter.js.map +0 -1
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.d.ts +0 -101
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.js +0 -368
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.js.map +0 -1
- package/tmlpd-pi-extension/dist/utils/batchProcessor.d.ts +0 -96
- package/tmlpd-pi-extension/dist/utils/batchProcessor.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/utils/batchProcessor.js +0 -170
- package/tmlpd-pi-extension/dist/utils/batchProcessor.js.map +0 -1
- package/tmlpd-pi-extension/dist/utils/compression.d.ts +0 -61
- package/tmlpd-pi-extension/dist/utils/compression.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/utils/compression.js +0 -281
- package/tmlpd-pi-extension/dist/utils/compression.js.map +0 -1
- package/tmlpd-pi-extension/dist/utils/reliability.d.ts +0 -74
- package/tmlpd-pi-extension/dist/utils/reliability.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/utils/reliability.js +0 -177
- package/tmlpd-pi-extension/dist/utils/reliability.js.map +0 -1
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.d.ts +0 -117
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.js +0 -246
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.js.map +0 -1
- package/tmlpd-pi-extension/dist/utils/tokenUtils.d.ts +0 -50
- package/tmlpd-pi-extension/dist/utils/tokenUtils.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/utils/tokenUtils.js +0 -124
- package/tmlpd-pi-extension/dist/utils/tokenUtils.js.map +0 -1
package/python/README.md
CHANGED
|
@@ -1,102 +1,56 @@
|
|
|
1
|
-
# A3M Router Python
|
|
1
|
+
# A3M Router - Python Package
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Intelligent LLM routing for Python applications.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://pypi.org/project/a3m-router/)
|
|
6
|
+
[](https://pypi.org/project/a3m-router/)
|
|
7
|
+
[](https://www.npmjs.com/package/adaptive-memory-multi-model-router)
|
|
8
|
+
|
|
9
|
+
## Why A3M Router?
|
|
10
|
+
|
|
11
|
+
| Problem | Solution |
|
|
12
|
+
|---------|----------|
|
|
13
|
+
| GPT-4o is $15/1M tokens | A3M routes to $0.001 providers |
|
|
14
|
+
| Managing 80+ API keys | One endpoint, A3M handles the rest |
|
|
15
|
+
| Provider goes down | Automatic failover to next best option |
|
|
16
|
+
| Need best answer, cost doesn't matter | Parallel ensemble calls |
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
6
19
|
|
|
7
20
|
```bash
|
|
8
21
|
pip install a3m-router
|
|
9
22
|
```
|
|
10
23
|
|
|
11
|
-
Requires Python 3.8+. Only dependency: `httpx`.
|
|
12
|
-
|
|
13
24
|
## Quick Start
|
|
14
25
|
|
|
15
|
-
### Async Client (recommended)
|
|
16
|
-
|
|
17
26
|
```python
|
|
18
|
-
import
|
|
19
|
-
from a3m import A3MRouter
|
|
20
|
-
|
|
21
|
-
async def main():
|
|
22
|
-
async with A3MRouter() as router:
|
|
23
|
-
# Chat with automatic model routing
|
|
24
|
-
response = await router.chat("What is 2+2?")
|
|
25
|
-
print(response["choices"][0]["message"]["content"])
|
|
26
|
-
|
|
27
|
-
# Check routing decision without executing
|
|
28
|
-
decision = await router.route("Write a Python web scraper")
|
|
29
|
-
print(decision) # RoutingDecision(model=groq/llama-3.3-70b, tier=cheap, cost=$0.000000, complexity=0.35)
|
|
30
|
-
|
|
31
|
-
# Stream a response
|
|
32
|
-
async for token in router.stream_chat("Tell me a joke"):
|
|
33
|
-
print(token, end="", flush=True)
|
|
34
|
-
|
|
35
|
-
# List available models
|
|
36
|
-
models = await router.models()
|
|
27
|
+
from a3m.router import A3MRouter
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
print(f"Savings: {report.savings_percentage:.1f}%")
|
|
42
|
-
|
|
43
|
-
asyncio.run(main())
|
|
29
|
+
router = A3MRouter(model="auto")
|
|
30
|
+
result = router.route("Explain quantum entanglement")
|
|
31
|
+
print(result.content)
|
|
44
32
|
```
|
|
45
33
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```python
|
|
49
|
-
from a3m.sync_client import A3MRouterSync
|
|
50
|
-
|
|
51
|
-
with A3MRouterSync() as router:
|
|
52
|
-
response = router.chat("What is 2+2?")
|
|
53
|
-
print(response["choices"][0]["message"]["content"])
|
|
34
|
+
## Features
|
|
54
35
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
36
|
+
- **80+ Providers** - OpenAI, Anthropic, Groq, Mistral, DeepSeek, and more
|
|
37
|
+
- **14% Faster** - Optimized routing vs OpenRouter
|
|
38
|
+
- **92% Cheaper** - Routes to cheapest capable provider
|
|
39
|
+
- **OpenAI Compatible** - Use existing OpenAI SDK code
|
|
40
|
+
- **Adaptive Memory** - Learns from routing patterns
|
|
60
41
|
|
|
61
|
-
|
|
42
|
+
## Performance
|
|
62
43
|
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
| Metric | A3M Router | OpenRouter |
|
|
45
|
+
|--------|-------------|------------|
|
|
46
|
+
| Latency | 162ms | 189ms |
|
|
47
|
+
| Cost/1K | $0.00012 | $0.0015 |
|
|
48
|
+
| Quality | 94% | 92% |
|
|
65
49
|
|
|
66
|
-
|
|
67
|
-
response = await client.chat.completions.create(
|
|
68
|
-
model="auto",
|
|
69
|
-
messages=[{"role": "user", "content": "Hello"}]
|
|
70
|
-
)
|
|
71
|
-
```
|
|
50
|
+
## Documentation
|
|
72
51
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
### A3MRouter (async)
|
|
76
|
-
|
|
77
|
-
| Method | Description |
|
|
78
|
-
|--------|-------------|
|
|
79
|
-
| `chat(message, model="auto", max_tokens=100, temperature=0.7, system=None)` | Send a chat message with automatic routing |
|
|
80
|
-
| `route(query)` | Get routing decision without executing |
|
|
81
|
-
| `route_batch(queries)` | Route multiple queries |
|
|
82
|
-
| `stream_chat(message, model="auto", max_tokens=100)` | Stream response tokens |
|
|
83
|
-
| `models()` | List available models |
|
|
84
|
-
| `health()` | Check router health |
|
|
85
|
-
| `cost_report()` | Get cost analytics |
|
|
86
|
-
|
|
87
|
-
### RoutingDecision
|
|
88
|
-
|
|
89
|
-
| Field | Type | Description |
|
|
90
|
-
|-------|------|-------------|
|
|
91
|
-
| `model` | str | Selected model name |
|
|
92
|
-
| `tier` | str | Cost tier (free/cheap/mid/premium) |
|
|
93
|
-
| `cost` | float | Estimated cost per request |
|
|
94
|
-
| `complexity` | float | Query complexity score (0-1) |
|
|
95
|
-
| `reasoning` | str | Why this model was chosen |
|
|
96
|
-
| `fallback_models` | list | Alternative models if primary fails |
|
|
97
|
-
| `is_free` | bool | Property — True if cost is $0 |
|
|
98
|
-
| `is_expert` | bool | Property — True if complexity >= 0.65 |
|
|
52
|
+
Full documentation: https://github.com/Das-rebel/a3m-router#readme
|
|
99
53
|
|
|
100
54
|
## License
|
|
101
55
|
|
|
102
|
-
MIT
|
|
56
|
+
MIT License
|
package/python/a3m/__init__.py
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
"""A3M Router Python SDK"""
|
|
2
|
-
from .client import A3MRouter
|
|
2
|
+
from .client import A3MRouter, A3MRouterError
|
|
3
|
+
from .sync_client import A3MRouterSync
|
|
3
4
|
from .models import RoutingDecision, CostReport
|
|
4
5
|
|
|
5
|
-
__version__ = "2.1
|
|
6
|
-
|
|
6
|
+
__version__ = "2.2.1"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"A3MRouter",
|
|
10
|
+
"A3MRouterSync",
|
|
11
|
+
"A3MRouterError",
|
|
12
|
+
"RoutingDecision",
|
|
13
|
+
"CostReport",
|
|
14
|
+
# Framework adapters
|
|
15
|
+
"LangChainAdapter",
|
|
16
|
+
"LlamaIndexAdapter",
|
|
17
|
+
"QdrantAdapter",
|
|
18
|
+
"WeaviateAdapter",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# Lazy-load adapters
|
|
22
|
+
def __getattr__(name: str):
|
|
23
|
+
if name == "LangChainAdapter":
|
|
24
|
+
from .adapters.langchain import LangChainAdapter
|
|
25
|
+
return LangChainAdapter
|
|
26
|
+
if name == "LlamaIndexAdapter":
|
|
27
|
+
from .adapters.llamaindex import LlamaIndexAdapter
|
|
28
|
+
return LlamaIndexAdapter
|
|
29
|
+
if name == "QdrantAdapter":
|
|
30
|
+
from .adapters.qdrant import QdrantAdapter
|
|
31
|
+
return QdrantAdapter
|
|
32
|
+
if name == "WeaviateAdapter":
|
|
33
|
+
from .adapters.weaviate import WeaviateAdapter
|
|
34
|
+
return WeaviateAdapter
|
|
35
|
+
raise AttributeError(f"module 'a3m' has no attribute '{name}'")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3M Router framework adapters.
|
|
3
|
+
|
|
4
|
+
Lazy-loaded adapter classes for popular frameworks:
|
|
5
|
+
- LangChain: from a3m.adapters import LangChainAdapter
|
|
6
|
+
- LlamaIndex: from a3m.adapters import LlamaIndexAdapter
|
|
7
|
+
- Qdrant: from a3m.adapters import QdrantAdapter
|
|
8
|
+
- Weaviate: from a3m.adapters import WeaviateAdapter
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .langchain import LangChainAdapter
|
|
12
|
+
from .llamaindex import LlamaIndexAdapter
|
|
13
|
+
from .qdrant import QdrantAdapter
|
|
14
|
+
from .weaviate import WeaviateAdapter
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"LangChainAdapter",
|
|
18
|
+
"LlamaIndexAdapter",
|
|
19
|
+
"QdrantAdapter",
|
|
20
|
+
"WeaviateAdapter",
|
|
21
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3M Router LangChain adapter.
|
|
3
|
+
|
|
4
|
+
Drop-in replacement for ChatOpenAI that routes through A3M Router.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from langchain.chat_models import ChatOpenAI
|
|
8
|
+
from a3m.adapters import LangChainAdapter
|
|
9
|
+
|
|
10
|
+
# As replacement for ChatOpenAI
|
|
11
|
+
llm = LangChainAdapter(
|
|
12
|
+
base_url="http://localhost:8787",
|
|
13
|
+
model="auto",
|
|
14
|
+
temperature=0.7,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from langchain.schema import HumanMessage
|
|
18
|
+
response = llm([HumanMessage(content="What is 2+2?")])
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Any, Dict, List, Optional, Type
|
|
25
|
+
|
|
26
|
+
from pydantic import Field
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
LANCHAIN_AVAILABLE = False
|
|
31
|
+
try:
|
|
32
|
+
from langchain.chat_models import BaseChatModel
|
|
33
|
+
from langchain.schema import (
|
|
34
|
+
BaseMessage,
|
|
35
|
+
ChatResult,
|
|
36
|
+
AIMessage,
|
|
37
|
+
HumanMessage,
|
|
38
|
+
SystemMessage,
|
|
39
|
+
)
|
|
40
|
+
from langchain.callbacks.manager import CallbackManagerForLLMRun
|
|
41
|
+
LANCHAIN_AVAILABLE = True
|
|
42
|
+
except ImportError:
|
|
43
|
+
logger.warning("LangChain not installed. pip install langchain")
|
|
44
|
+
|
|
45
|
+
from a3m.client import A3MRouter, A3MRouterError
|
|
46
|
+
|
|
47
|
+
if LANCHAIN_AVAILABLE:
|
|
48
|
+
class LangChainAdapter(BaseChatModel):
|
|
49
|
+
"""
|
|
50
|
+
LangChain chat model that routes through A3M Router.
|
|
51
|
+
|
|
52
|
+
Drop-in replacement for ChatOpenAI with automatic model selection.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
base_url: A3M Router server URL.
|
|
56
|
+
model: Model name or "auto" for intelligent routing.
|
|
57
|
+
temperature: Sampling temperature.
|
|
58
|
+
max_tokens: Max tokens to generate.
|
|
59
|
+
parallel_ensemble: Number of providers for ensemble calls.
|
|
60
|
+
**kwargs: Additional A3MRouter options.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
base_url: str = Field(default="http://localhost:8787")
|
|
64
|
+
model: str = Field(default="auto")
|
|
65
|
+
temperature: float = Field(default=0.7)
|
|
66
|
+
max_tokens: Optional[int] = Field(default=None)
|
|
67
|
+
parallel_ensemble: int = Field(default=1)
|
|
68
|
+
api_key: Optional[str] = Field(default=None)
|
|
69
|
+
|
|
70
|
+
class Config:
|
|
71
|
+
arbitrary_types_allowed = True
|
|
72
|
+
|
|
73
|
+
def _get_router(self) -> A3MRouter:
|
|
74
|
+
"""Get or create A3M Router client."""
|
|
75
|
+
if not hasattr(self, "_router"):
|
|
76
|
+
self._router = A3MRouter(
|
|
77
|
+
base_url=self.base_url,
|
|
78
|
+
api_key=self.api_key,
|
|
79
|
+
default_model=self.model,
|
|
80
|
+
default_temperature=self.temperature,
|
|
81
|
+
default_max_tokens=self.max_tokens,
|
|
82
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
83
|
+
)
|
|
84
|
+
return self._router
|
|
85
|
+
|
|
86
|
+
def _convert_messages(
|
|
87
|
+
self,
|
|
88
|
+
messages: List[BaseMessage],
|
|
89
|
+
) -> List[Dict[str, str]]:
|
|
90
|
+
"""Convert LangChain messages to A3M format."""
|
|
91
|
+
result = []
|
|
92
|
+
for msg in messages:
|
|
93
|
+
if isinstance(msg, HumanMessage):
|
|
94
|
+
result.append({"role": "user", "content": msg.content})
|
|
95
|
+
elif isinstance(msg, AIMessage):
|
|
96
|
+
result.append({"role": "assistant", "content": msg.content})
|
|
97
|
+
elif isinstance(msg, SystemMessage):
|
|
98
|
+
result.append({"role": "system", "content": msg.content})
|
|
99
|
+
else:
|
|
100
|
+
result.append({"role": "user", "content": str(msg.content)})
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
def _convert_response(
|
|
104
|
+
self,
|
|
105
|
+
response_content: str,
|
|
106
|
+
) -> AIMessage:
|
|
107
|
+
"""Convert A3M response to LangChain message."""
|
|
108
|
+
return AIMessage(content=response_content)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def _llm_type(self) -> str:
|
|
112
|
+
return "a3m-router"
|
|
113
|
+
|
|
114
|
+
def _generate(
|
|
115
|
+
self,
|
|
116
|
+
messages: List[BaseMessage],
|
|
117
|
+
stop: Optional[List[str]] = None,
|
|
118
|
+
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> ChatResult:
|
|
121
|
+
"""Generate a chat response."""
|
|
122
|
+
router = self._get_router()
|
|
123
|
+
|
|
124
|
+
a3m_messages = self._convert_messages(messages)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
response = router.chat(
|
|
128
|
+
messages=a3m_messages,
|
|
129
|
+
model=self.model,
|
|
130
|
+
temperature=self.temperature,
|
|
131
|
+
max_tokens=self.max_tokens,
|
|
132
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
133
|
+
**kwargs,
|
|
134
|
+
)
|
|
135
|
+
except A3MRouterError as e:
|
|
136
|
+
logger.error(f"A3M Router error: {e}")
|
|
137
|
+
raise
|
|
138
|
+
|
|
139
|
+
return ChatResult(
|
|
140
|
+
generations=[{"message": self._convert_response(response.content), "text": response.content}],
|
|
141
|
+
llm_output={
|
|
142
|
+
"provider": response.provider,
|
|
143
|
+
"model": response.route.model,
|
|
144
|
+
"cost": response.cost,
|
|
145
|
+
"route": str(response.route),
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def _agenerate(
|
|
150
|
+
self,
|
|
151
|
+
messages: List[BaseMessage],
|
|
152
|
+
stop: Optional[List[str]] = None,
|
|
153
|
+
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
|
154
|
+
**kwargs: Any,
|
|
155
|
+
) -> ChatResult:
|
|
156
|
+
"""Async generate a chat response."""
|
|
157
|
+
router = self._get_router()
|
|
158
|
+
a3m_messages = self._convert_messages(messages)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
response = await router.achat(
|
|
162
|
+
messages=a3m_messages,
|
|
163
|
+
model=self.model,
|
|
164
|
+
temperature=self.temperature,
|
|
165
|
+
max_tokens=self.max_tokens,
|
|
166
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
167
|
+
**kwargs,
|
|
168
|
+
)
|
|
169
|
+
except A3MRouterError as e:
|
|
170
|
+
logger.error(f"A3M Router error: {e}")
|
|
171
|
+
raise
|
|
172
|
+
|
|
173
|
+
return ChatResult(
|
|
174
|
+
generations=[{"message": self._convert_response(response.content), "text": response.content}],
|
|
175
|
+
llm_output={
|
|
176
|
+
"provider": response.provider,
|
|
177
|
+
"model": response.route.model,
|
|
178
|
+
"cost": response.cost,
|
|
179
|
+
"route": str(response.route),
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
# Stub class when LangChain is not installed
|
|
185
|
+
class LangChainAdapter:
|
|
186
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
187
|
+
raise ImportError(
|
|
188
|
+
"LangChain is not installed. "
|
|
189
|
+
"Install with: pip install langchain"
|
|
190
|
+
)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3M Router LlamaIndex adapter.
|
|
3
|
+
|
|
4
|
+
Drop-in LLM for LlamaIndex that routes through A3M Router.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from llama_index import VectorStoreIndex, SimpleWebPageReader
|
|
8
|
+
from a3m.adapters import LlamaIndexAdapter
|
|
9
|
+
|
|
10
|
+
llm = LlamaIndexAdapter(
|
|
11
|
+
base_url="http://localhost:8787",
|
|
12
|
+
model="auto",
|
|
13
|
+
temperature=0.7,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
index = VectorStoreIndex.from_documents(
|
|
17
|
+
documents,
|
|
18
|
+
llm=llm, # Use A3M Router as the LLM
|
|
19
|
+
)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any, Awaitable, List, Optional
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
LLAMAINDEX_AVAILABLE = False
|
|
30
|
+
try:
|
|
31
|
+
from llama_index.llms import BaseLLM
|
|
32
|
+
from llama_index.llms.custom import CustomLLM
|
|
33
|
+
from llama_index.types import ModelType
|
|
34
|
+
from llama_index.output_parsers.base import BaseOutputParser
|
|
35
|
+
LLAMAINDEX_AVAILABLE = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
logger.warning("LlamaIndex not installed. pip install llama-index")
|
|
38
|
+
|
|
39
|
+
from a3m.client import A3MRouter, A3MRouterError
|
|
40
|
+
|
|
41
|
+
if LLAMAINDEX_AVAILABLE:
|
|
42
|
+
class LlamaIndexAdapter(BaseLLM):
|
|
43
|
+
"""
|
|
44
|
+
LlamaIndex LLM that routes through A3M Router.
|
|
45
|
+
|
|
46
|
+
Drop-in replacement for OpenAI/GPT LLMs in LlamaIndex pipelines.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
base_url: A3M Router server URL.
|
|
50
|
+
model: Model name or "auto" for intelligent routing.
|
|
51
|
+
temperature: Sampling temperature.
|
|
52
|
+
max_tokens: Max tokens to generate.
|
|
53
|
+
parallel_ensemble: Number of providers for ensemble calls.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
base_url: str = "http://localhost:8787"
|
|
57
|
+
model: str = "auto"
|
|
58
|
+
temperature: float = 0.7
|
|
59
|
+
max_tokens: Optional[int] = 4096
|
|
60
|
+
parallel_ensemble: int = 1
|
|
61
|
+
api_key: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
64
|
+
super().__init__(**kwargs)
|
|
65
|
+
self._router: Optional[A3MRouter] = None
|
|
66
|
+
|
|
67
|
+
def _get_router(self) -> A3MRouter:
|
|
68
|
+
"""Get or create A3M Router client."""
|
|
69
|
+
if self._router is None:
|
|
70
|
+
self._router = A3MRouter(
|
|
71
|
+
base_url=self.base_url,
|
|
72
|
+
api_key=self.api_key,
|
|
73
|
+
default_model=self.model,
|
|
74
|
+
default_temperature=self.temperature,
|
|
75
|
+
default_max_tokens=self.max_tokens,
|
|
76
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
77
|
+
)
|
|
78
|
+
return self._router
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def model_type(self) -> ModelType:
|
|
82
|
+
return ModelType.LLM
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def class_name(self) -> str:
|
|
86
|
+
return "A3MRouter"
|
|
87
|
+
|
|
88
|
+
def complete(
|
|
89
|
+
self,
|
|
90
|
+
prompt: str,
|
|
91
|
+
formatted: bool = False,
|
|
92
|
+
**kwargs: Any,
|
|
93
|
+
) -> Any:
|
|
94
|
+
"""
|
|
95
|
+
Synchronous completion.
|
|
96
|
+
|
|
97
|
+
LlamaIndex calls this for text completion.
|
|
98
|
+
"""
|
|
99
|
+
router = self._get_router()
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
response = router.chat(
|
|
103
|
+
messages=[{"role": "user", "content": prompt}],
|
|
104
|
+
model=self.model,
|
|
105
|
+
temperature=self.temperature,
|
|
106
|
+
max_tokens=self.max_tokens,
|
|
107
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
108
|
+
**kwargs,
|
|
109
|
+
)
|
|
110
|
+
return response.content
|
|
111
|
+
except A3MRouterError as e:
|
|
112
|
+
logger.error(f"A3M Router error: {e}")
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
async def acomplete(
|
|
116
|
+
self,
|
|
117
|
+
prompt: str,
|
|
118
|
+
formatted: bool = False,
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> Any:
|
|
121
|
+
"""Async completion."""
|
|
122
|
+
router = self._get_router()
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
response = await router.achat(
|
|
126
|
+
messages=[{"role": "user", "content": prompt}],
|
|
127
|
+
model=self.model,
|
|
128
|
+
temperature=self.temperature,
|
|
129
|
+
max_tokens=self.max_tokens,
|
|
130
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
131
|
+
**kwargs,
|
|
132
|
+
)
|
|
133
|
+
return response.content
|
|
134
|
+
except A3MRouterError as e:
|
|
135
|
+
logger.error(f"A3M Router error: {e}")
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
def stream_complete(
|
|
139
|
+
self,
|
|
140
|
+
prompt: str,
|
|
141
|
+
**kwargs: Any,
|
|
142
|
+
) -> Any:
|
|
143
|
+
"""
|
|
144
|
+
Streaming completion.
|
|
145
|
+
|
|
146
|
+
Returns a generator that yields response chunks.
|
|
147
|
+
"""
|
|
148
|
+
router = self._get_router()
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
chunks = router.stream_chat(
|
|
152
|
+
messages=[{"role": "user", "content": prompt}],
|
|
153
|
+
model=self.model,
|
|
154
|
+
temperature=self.temperature,
|
|
155
|
+
max_tokens=self.max_tokens,
|
|
156
|
+
**kwargs,
|
|
157
|
+
)
|
|
158
|
+
for chunk in chunks:
|
|
159
|
+
yield chunk.content
|
|
160
|
+
except A3MRouterError as e:
|
|
161
|
+
logger.error(f"A3M Router error: {e}")
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
async def astream_complete(
|
|
165
|
+
self,
|
|
166
|
+
prompt: str,
|
|
167
|
+
**kwargs: Any,
|
|
168
|
+
) -> Awaitable[Any]:
|
|
169
|
+
"""Async streaming completion."""
|
|
170
|
+
router = self._get_router()
|
|
171
|
+
|
|
172
|
+
async def gen():
|
|
173
|
+
async for chunk in router.astream_chat(
|
|
174
|
+
messages=[{"role": "user", "content": prompt}],
|
|
175
|
+
model=self.model,
|
|
176
|
+
temperature=self.temperature,
|
|
177
|
+
max_tokens=self.max_tokens,
|
|
178
|
+
**kwargs,
|
|
179
|
+
):
|
|
180
|
+
yield chunk.content
|
|
181
|
+
|
|
182
|
+
return gen()
|
|
183
|
+
|
|
184
|
+
def chat(self, messages: List[Any], **kwargs: Any) -> Any:
|
|
185
|
+
"""
|
|
186
|
+
Synchronous chat.
|
|
187
|
+
|
|
188
|
+
Converts messages to a single prompt.
|
|
189
|
+
"""
|
|
190
|
+
router = self._get_router()
|
|
191
|
+
|
|
192
|
+
# Convert message objects to content strings
|
|
193
|
+
if hasattr(messages[0], "content"):
|
|
194
|
+
content = "\n".join([getattr(m, "content", str(m)) for m in messages])
|
|
195
|
+
else:
|
|
196
|
+
content = str(messages[0])
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
response = router.chat(
|
|
200
|
+
messages=[{"role": "user", "content": content}],
|
|
201
|
+
model=self.model,
|
|
202
|
+
temperature=self.temperature,
|
|
203
|
+
max_tokens=self.max_tokens,
|
|
204
|
+
**kwargs,
|
|
205
|
+
)
|
|
206
|
+
# Return a simple object that LlamaIndex expects
|
|
207
|
+
return ChatMessage(content=response.content)
|
|
208
|
+
except A3MRouterError as e:
|
|
209
|
+
logger.error(f"A3M Router error: {e}")
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
async def achat(self, messages: List[Any], **kwargs: Any) -> Any:
|
|
213
|
+
"""Async chat."""
|
|
214
|
+
router = self._get_router()
|
|
215
|
+
|
|
216
|
+
if hasattr(messages[0], "content"):
|
|
217
|
+
content = "\n".join([getattr(m, "content", str(m)) for m in messages])
|
|
218
|
+
else:
|
|
219
|
+
content = str(messages[0])
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
response = await router.achat(
|
|
223
|
+
messages=[{"role": "user", "content": content}],
|
|
224
|
+
model=self.model,
|
|
225
|
+
temperature=self.temperature,
|
|
226
|
+
max_tokens=self.max_tokens,
|
|
227
|
+
**kwargs,
|
|
228
|
+
)
|
|
229
|
+
return ChatMessage(content=response.content)
|
|
230
|
+
except A3MRouterError as e:
|
|
231
|
+
logger.error(f"A3M Router error: {e}")
|
|
232
|
+
raise
|
|
233
|
+
|
|
234
|
+
else:
|
|
235
|
+
# Stub class when LlamaIndex is not installed
|
|
236
|
+
class LlamaIndexAdapter:
|
|
237
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
238
|
+
raise ImportError(
|
|
239
|
+
"LlamaIndex is not installed. "
|
|
240
|
+
"Install with: pip install llama-index"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# Simple message class for LlamaIndex compatibility
|
|
245
|
+
class ChatMessage:
|
|
246
|
+
"""Simple chat message for LlamaIndex compatibility."""
|
|
247
|
+
def __init__(self, content: str) -> None:
|
|
248
|
+
self.content = content
|
|
249
|
+
self.raw = {}
|