adaptive-memory-multi-model-router 2.15.3 → 2.15.4
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/ISSUE_TEMPLATE/bug_report.md +94 -0
- package/.github/ISSUE_TEMPLATE/config.yml +17 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
- package/.well-known/ai-plugin.json +16 -0
- package/AGENT_COUNCIL_FINDINGS.md +98 -123
- package/POPULARITY_BOOSTERS.md +1 -1
- package/README.md +275 -147
- package/README_ja.md +20 -16
- package/README_zh.md +17 -13
- package/adapters/README.md +36 -0
- package/adapters/__init__.py +25 -0
- package/adapters/a3m_adapter/__init__.py +15 -0
- package/adapters/a3m_adapter/adapter/__init__.py +7 -0
- package/adapters/a3m_adapter/adapter/config.py +100 -0
- package/adapters/a3m_adapter/adapter/langchain.py +155 -0
- package/adapters/a3m_adapter/adapter/llamaindex.py +162 -0
- package/adapters/a3m_adapter/tests/__init__.py +1 -0
- package/adapters/a3m_adapter/tests/test_adapters.py +120 -0
- package/adapters/setup.py +23 -0
- package/articles/CHINESE_DIRECTORIES.md +2 -2
- package/articles/CHINESE_SUBMISSIONS_READY.md +5 -5
- package/articles/HN_POST_READY.md +20 -16
- package/articles/INDIEHACKERS_READY.md +34 -19
- package/dist/analytics/costAnalytics.d.ts +1 -0
- package/dist/benchmark/reproducible.d.ts +1 -0
- package/dist/cache/semanticCache.d.ts +1 -0
- package/dist/cost/budgetEnforcer.d.ts +1 -0
- package/dist/cost/costTracker.d.ts +1 -0
- package/dist/ensemble/multiRoundDialog.d.ts +1 -0
- package/dist/ensemble/shapleyValue.d.ts +1 -0
- package/dist/ensemble.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/integrations/oauth.d.ts +1 -0
- package/dist/integrations/scienceAdapter.d.ts +1 -0
- package/dist/memory/autoFetch.d.ts +1 -0
- package/dist/memory/hybridMemory.d.ts +1 -0
- package/dist/memory/memoryTree.d.ts +1 -0
- package/dist/memory/obsidianVault.d.ts +1 -0
- package/dist/memory/reasoningBank.d.ts +1 -0
- package/dist/observability/changeWatch.d.ts +1 -0
- package/dist/observability/fatigueDetector.d.ts +1 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/metrics.d.ts +1 -0
- package/dist/observability/middleware.d.ts +1 -0
- package/dist/observability/tracer.d.ts +1 -0
- package/dist/observability/types.d.ts +1 -0
- package/dist/providers/providerConfig.d.ts +1 -0
- package/dist/routing/advancedRouter.d.ts +2 -1
- package/dist/routing/advancedRouter.js +0 -84
- package/dist/routing/advancedRouter.js.map +1 -1
- package/dist/routing/crossModelValidation.d.ts +1 -0
- package/dist/routing/providerHealth.d.ts +2 -77
- package/dist/routing/providerHealth.js +4 -190
- package/dist/routing/providerHealth.js.map +1 -1
- package/dist/routing/providerRetry.d.ts +1 -0
- package/dist/sdk.d.ts +1 -0
- package/dist/security/guardrails.d.ts +1 -0
- package/dist/server/dashboard.d.ts +1 -0
- package/dist/server/handlers/chatHandler.d.ts +1 -0
- package/dist/server/handlers/completionsHandler.d.ts +1 -0
- package/dist/server/handlers/embeddingsHandler.d.ts +1 -0
- package/dist/server/handlers/healthHandler.d.ts +1 -0
- package/dist/server/handlers/metricsHandler.d.ts +1 -0
- package/dist/server/handlers/modelsHandler.d.ts +1 -0
- package/dist/server/metrics.d.ts +1 -0
- package/dist/server/modelMapper.d.ts +1 -0
- package/dist/server/proxyServer.d.ts +1 -0
- package/dist/server/router.d.ts +1 -0
- package/dist/server/state.d.ts +1 -0
- package/dist/tui/dashboard.d.ts +1 -0
- package/dist/utils/costUtils.d.ts +1 -0
- package/dist/utils/sorting.d.ts +1 -0
- package/dist/utils/tokenUtils.d.ts +1 -0
- package/docs/llms-full.txt +286 -153
- package/docs/llms.txt +112 -71
- package/llms-full.txt +6 -6
- package/llms.txt +112 -62
- package/package.json +12 -91
- package/CAMPAIGN_SUMMARY.md +0 -87
- package/ENTERPRISE_INTEGRATIONS.md +0 -94
- package/INTEGRATION_PROGRESS.md +0 -96
- package/OPPORTUNITIES_100.md +0 -277
- package/TODO_BROWSER_AUTOMATION.md +0 -87
- package/a3m-integrations-summary.md +0 -73
- package/dist/routing/shadowSampler.d.ts +0 -187
- package/dist/routing/shadowSampler.js +0 -306
- package/dist/routing/shadowSampler.js.map +0 -1
- package/integrations/browser-use/README.md +0 -155
- package/integrations/browser-use/examples/form-filling.ts +0 -179
- package/integrations/browser-use/index.ts +0 -145
- package/integrations/browser-use/package.json +0 -34
- package/integrations/browser-use/tsconfig.json +0 -17
- package/integrations/mcp-browser/README.md +0 -177
- package/integrations/mcp-browser/package.json +0 -38
- package/integrations/mcp-browser/src/index.ts +0 -297
- package/integrations/sota-browser/README.md +0 -191
- package/integrations/sota-browser/package.json +0 -34
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# A3M Router Adapters
|
|
2
|
+
|
|
3
|
+
Drop-in adapters for LangChain and LlamaIndex to integrate with A3M Router for intelligent model routing.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install a3m_adapter
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install with extras:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install a3m_adapter[langchain] # With LangChain support
|
|
15
|
+
pip install a3m_adapter[llamaindex] # With LlamaIndex support
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### LangChain
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from a3m_adapter import A3MLangChainAdapter
|
|
24
|
+
|
|
25
|
+
llm = A3MLangChainAdapter(model="auto", temperature=0.7)
|
|
26
|
+
result = llm.invoke("What is the capital of France?")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### LlamaIndex
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from a3m_adapter import A3MLlamaIndexAdapter
|
|
33
|
+
|
|
34
|
+
llm = A3MLlamaIndexAdapter(model="auto")
|
|
35
|
+
response = llm.complete("What is the capital of France?")
|
|
36
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3M Router Adapter Package
|
|
3
|
+
|
|
4
|
+
This package provides drop-in adapters to integrate A3M Router
|
|
5
|
+
with popular LLM frameworks including LangChain, LlamaIndex, and more.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from adapters import A3MLangChainAdapter, A3MLlamaIndexAdapter, A3MConfig
|
|
9
|
+
|
|
10
|
+
# LangChain
|
|
11
|
+
llm = A3MLangChainAdapter(model="auto", temperature=0.7)
|
|
12
|
+
|
|
13
|
+
# LlamaIndex
|
|
14
|
+
llm = A3MLlamaIndexAdapter(model="auto")
|
|
15
|
+
|
|
16
|
+
# Configuration
|
|
17
|
+
config = A3MConfig(model="auto", parallel_ensemble=2)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .a3m_adapter.adapter.langchain import A3MLangChainAdapter
|
|
21
|
+
from .a3m_adapter.adapter.llamaindex import A3MLlamaIndexAdapter
|
|
22
|
+
from .a3m_adapter.adapter.config import A3MConfig
|
|
23
|
+
|
|
24
|
+
__all__ = ['A3MLangChainAdapter', 'A3MLlamaIndexAdapter', 'A3MConfig']
|
|
25
|
+
__version__ = '1.0.0'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3M Router Adapters for LLM Frameworks.
|
|
3
|
+
|
|
4
|
+
Provides drop-in adapters for:
|
|
5
|
+
- LangChain (A3MLangChainAdapter)
|
|
6
|
+
- LlamaIndex (A3MLlamaIndexAdapter)
|
|
7
|
+
- Configuration management (A3MConfig)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .adapter.langchain import A3MLangChainAdapter
|
|
11
|
+
from .adapter.llamaindex import A3MLlamaIndexAdapter
|
|
12
|
+
from .adapter.config import A3MConfig
|
|
13
|
+
|
|
14
|
+
__all__ = ['A3MLangChainAdapter', 'A3MLlamaIndexAdapter', 'A3MConfig']
|
|
15
|
+
__version__ = '1.0.0'
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for A3M Router adapters.
|
|
3
|
+
|
|
4
|
+
Provides settings for:
|
|
5
|
+
- Default model selection strategy
|
|
6
|
+
- Cost optimization thresholds
|
|
7
|
+
- Parallel ensemble settings
|
|
8
|
+
- Provider priority lists
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from a3m_adapter_config import A3MConfig
|
|
12
|
+
|
|
13
|
+
config = A3MConfig.from_file("a3m_config.yaml")
|
|
14
|
+
llm = A3MChatModel(**config.to_dict())
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass, field, asdict
|
|
22
|
+
from typing import Any, Dict, List, Optional, Union
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class A3MConfig:
|
|
29
|
+
"""Configuration for A3M Router adapters."""
|
|
30
|
+
|
|
31
|
+
# Model selection
|
|
32
|
+
model: str = "auto"
|
|
33
|
+
|
|
34
|
+
# Sampling parameters
|
|
35
|
+
temperature: float = 0.0
|
|
36
|
+
max_tokens: Optional[int] = 4096
|
|
37
|
+
top_p: float = 1.0
|
|
38
|
+
frequency_penalty: float = 0.0
|
|
39
|
+
presence_penalty: float = 0.0
|
|
40
|
+
|
|
41
|
+
# Routing strategy
|
|
42
|
+
parallel_ensemble: int = 1
|
|
43
|
+
fallback_enabled: bool = True
|
|
44
|
+
cost_threshold: float = 0.05 # Max $ per 1k tokens
|
|
45
|
+
|
|
46
|
+
# Provider preferences (highest priority first)
|
|
47
|
+
preferred_providers: List[str] = field(default_factory=lambda: [
|
|
48
|
+
"openai", "anthropic", "google", "azure_openai",
|
|
49
|
+
"azure_ais", "litellm", "groq", "together"
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
# Excluded providers (never use)
|
|
53
|
+
excluded_providers: List[str] = field(default_factory=lambda: [])
|
|
54
|
+
|
|
55
|
+
# API configuration
|
|
56
|
+
api_endpoint: str = "http://localhost:8787/v1"
|
|
57
|
+
api_key: Optional[str] = None
|
|
58
|
+
|
|
59
|
+
# Budget controls
|
|
60
|
+
monthly_budget_usd: Optional[float] = None
|
|
61
|
+
daily_budget_usd: Optional[float] = None
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_file(cls, path: str) -> "A3MConfig":
|
|
65
|
+
"""Load configuration from YAML file."""
|
|
66
|
+
try:
|
|
67
|
+
import yaml
|
|
68
|
+
with open(path, 'r') as f:
|
|
69
|
+
data = yaml.safe_load(f)
|
|
70
|
+
return cls(**data)
|
|
71
|
+
except ImportError:
|
|
72
|
+
logger.warning("PyYAML not installed, using JSON")
|
|
73
|
+
return cls.from_json(path)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_json(cls, path: str) -> "A3MConfig":
|
|
77
|
+
"""Load configuration from JSON file."""
|
|
78
|
+
with open(path, 'r') as f:
|
|
79
|
+
data = json.load(f)
|
|
80
|
+
return cls(**data)
|
|
81
|
+
|
|
82
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
83
|
+
"""Convert to dictionary."""
|
|
84
|
+
return asdict(self)
|
|
85
|
+
|
|
86
|
+
def to_json(self, path: Optional[str] = None) -> Optional[str]:
|
|
87
|
+
"""Convert to JSON string or save to file."""
|
|
88
|
+
data = json.dumps(self.to_dict(), indent=2)
|
|
89
|
+
if path:
|
|
90
|
+
with open(path, 'w') as f:
|
|
91
|
+
f.write(data)
|
|
92
|
+
return data
|
|
93
|
+
|
|
94
|
+
def update_budget_limits(self, remaining_usd: float) -> None:
|
|
95
|
+
"""Update budget limits based on remaining funds."""
|
|
96
|
+
if self.daily_budget_usd is not None:
|
|
97
|
+
remaining_pct = remaining_usd / self.daily_budget_usd
|
|
98
|
+
if remaining_pct < 0.1:
|
|
99
|
+
logger.warning("Low daily budget: %s remaining", remaining_usd)
|
|
100
|
+
self.parallel_ensemble = 1 # Reduce to single-provider
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3M Router Adapter for LangChain.
|
|
3
|
+
|
|
4
|
+
Drop-in replacement for LangChain's ChatOpenAI that routes through A3M Router
|
|
5
|
+
for intelligent, cost-optimized model selection across 47+ providers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Check availability
|
|
16
|
+
LANGCHAIN_AVAILABLE = False
|
|
17
|
+
try:
|
|
18
|
+
from langchain_core.language_models import BaseChatModel
|
|
19
|
+
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
|
|
20
|
+
from langchain_core.outputs import ChatGeneration, ChatResult, LLMResult
|
|
21
|
+
LANGCHAIN_AVAILABLE = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
logger.warning("LangChain not installed. Install with: pip install langchain langchain-core")
|
|
24
|
+
|
|
25
|
+
A3M_AVAILABLE = False
|
|
26
|
+
try:
|
|
27
|
+
from a3m.router import A3MRouter, RouteResponse
|
|
28
|
+
A3M_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
logger.warning("A3M Router not installed. Install with: pip install adaptive-memory-multi-model-router")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class A3MLangChainAdapter:
|
|
34
|
+
"""
|
|
35
|
+
A3M Router adapter for LangChain's ChatOpenAI interface.
|
|
36
|
+
|
|
37
|
+
Routes prompts through A3M Router to automatically select the cheapest
|
|
38
|
+
capable model across 47+ LLM providers.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
model: str = "auto",
|
|
44
|
+
temperature: float = 0.0,
|
|
45
|
+
max_tokens: Optional[int] = 4096,
|
|
46
|
+
parallel_ensemble: int = 1,
|
|
47
|
+
api_key: Optional[str] = None,
|
|
48
|
+
**kwargs: Any,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Initialize A3M Router adapter.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
model: Model name or "auto" for automatic routing
|
|
55
|
+
temperature: Sampling temperature
|
|
56
|
+
max_tokens: Maximum tokens to generate
|
|
57
|
+
parallel_ensemble: Number of providers to run in parallel
|
|
58
|
+
api_key: A3M API key (optional)
|
|
59
|
+
"""
|
|
60
|
+
self.model = model
|
|
61
|
+
self.temperature = temperature
|
|
62
|
+
self.max_tokens = max_tokens
|
|
63
|
+
self.parallel_ensemble = parallel_ensemble
|
|
64
|
+
self.api_key = api_key
|
|
65
|
+
self._a3m_router = None
|
|
66
|
+
self._initialized = False
|
|
67
|
+
|
|
68
|
+
def _ensure_router(self) -> None:
|
|
69
|
+
"""Lazily initialize the A3M router."""
|
|
70
|
+
if self._initialized:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
if not A3M_AVAILABLE:
|
|
74
|
+
raise ImportError(
|
|
75
|
+
"A3M Router is not installed. "
|
|
76
|
+
"Install with: pip install adaptive-memory-multi-model-router"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self._a3m_router = A3MRouter(
|
|
80
|
+
model=self.model,
|
|
81
|
+
temperature=self.temperature,
|
|
82
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
83
|
+
)
|
|
84
|
+
self._initialized = True
|
|
85
|
+
logger.info(
|
|
86
|
+
"A3M Router initialized: model=%s, ensemble=%d",
|
|
87
|
+
self.model,
|
|
88
|
+
self.parallel_ensemble,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def _llm_type(self) -> str:
|
|
93
|
+
return "a3m_router"
|
|
94
|
+
|
|
95
|
+
def _generate(
|
|
96
|
+
self,
|
|
97
|
+
messages: List[BaseMessage],
|
|
98
|
+
stop: Optional[List[str]] = None,
|
|
99
|
+
run_manager: Any = None,
|
|
100
|
+
**kwargs: Any,
|
|
101
|
+
) -> LLMResult:
|
|
102
|
+
"""Generate a response using A3M Router."""
|
|
103
|
+
self._ensure_router()
|
|
104
|
+
|
|
105
|
+
# Convert messages
|
|
106
|
+
a3m_messages = self._convert_messages(messages)
|
|
107
|
+
|
|
108
|
+
# Route through A3M
|
|
109
|
+
import asyncio
|
|
110
|
+
loop = asyncio.get_event_loop()
|
|
111
|
+
route_result = loop.run_in_executor(
|
|
112
|
+
None,
|
|
113
|
+
lambda: self._a3m_router.route(
|
|
114
|
+
messages=a3m_messages,
|
|
115
|
+
temperature=self.temperature,
|
|
116
|
+
max_tokens=self.max_tokens,
|
|
117
|
+
stop=stop,
|
|
118
|
+
**kwargs,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
ai_message = AIMessage(content=route_result.content)
|
|
123
|
+
generation = ChatGeneration(message=ai_message)
|
|
124
|
+
return LLMResult(generations=[[generation]])
|
|
125
|
+
|
|
126
|
+
def _convert_messages(self, messages: List[BaseMessage]) -> List[Dict[str, Any]]:
|
|
127
|
+
"""Convert LangChain messages to A3M format."""
|
|
128
|
+
a3m_messages = []
|
|
129
|
+
for msg in messages:
|
|
130
|
+
if isinstance(msg, SystemMessage):
|
|
131
|
+
a3m_messages.append({"role": "system", "content": msg.content})
|
|
132
|
+
elif isinstance(msg, HumanMessage):
|
|
133
|
+
a3m_messages.append({"role": "user", "content": msg.content})
|
|
134
|
+
elif isinstance(msg, AIMessage):
|
|
135
|
+
a3m_messages.append({"role": "assistant", "content": msg.content})
|
|
136
|
+
elif isinstance(msg, ToolMessage):
|
|
137
|
+
a3m_messages.append(
|
|
138
|
+
{"role": "tool", "content": msg.content, "tool_call_id": msg.tool_call_id}
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
a3m_messages.append({"role": "user", "content": str(msg)})
|
|
142
|
+
return a3m_messages
|
|
143
|
+
|
|
144
|
+
def bind_tools(self, tools: List[Dict[str, Any]], **kwargs: Any) -> "A3MLangChainAdapter":
|
|
145
|
+
"""Bind tools for function calling."""
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def __repr__(self) -> str:
|
|
149
|
+
return (
|
|
150
|
+
f"A3MLangChainAdapter("
|
|
151
|
+
f"model={self.model!r}, "
|
|
152
|
+
f"temperature={self.temperature}, "
|
|
153
|
+
f"max_tokens={self.max_tokens}, "
|
|
154
|
+
f"ensemble={self.parallel_ensemble})"
|
|
155
|
+
)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3M Router Adapter for LlamaIndex.
|
|
3
|
+
|
|
4
|
+
Drop-in replacement for LlamaIndex's BaseLLM that routes through A3M Router
|
|
5
|
+
for intelligent, cost-optimized model selection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Check availability
|
|
16
|
+
LLAMAINDEX_AVAILABLE = False
|
|
17
|
+
_llama_metadata_class = None
|
|
18
|
+
try:
|
|
19
|
+
from llama_index.core.base.llms.base import BaseLLM, CompletionResponse
|
|
20
|
+
from llama_index.core.base.llms.types import ChatMessage
|
|
21
|
+
LLAMAINDEX_AVAILABLE = True
|
|
22
|
+
try:
|
|
23
|
+
from llama_index.core.base.llms.base import LLMMetadata
|
|
24
|
+
_llama_metadata_class = LLMMetadata
|
|
25
|
+
except ImportError:
|
|
26
|
+
pass
|
|
27
|
+
except ImportError:
|
|
28
|
+
logger.warning("LlamaIndex not installed. Install with: pip install llama-index")
|
|
29
|
+
|
|
30
|
+
A3M_AVAILABLE = False
|
|
31
|
+
try:
|
|
32
|
+
from a3m.router import A3MRouter, RouteResponse
|
|
33
|
+
A3M_AVAILABLE = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
logger.warning("A3M Router not installed. Install with: pip install adaptive-memory-multi-model-router")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class A3MLlamaIndexAdapter:
|
|
39
|
+
"""
|
|
40
|
+
A3M Router adapter for LlamaIndex's BaseLLM interface.
|
|
41
|
+
|
|
42
|
+
Routes prompts through A3M Router to automatically select the cheapest
|
|
43
|
+
capable model across 47+ LLM providers.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
model: str = "auto",
|
|
49
|
+
temperature: float = 0.0,
|
|
50
|
+
max_tokens: Optional[int] = 4096,
|
|
51
|
+
parallel_ensemble: int = 1,
|
|
52
|
+
api_key: Optional[str] = None,
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Initialize A3M Router adapter.
|
|
57
|
+
"""
|
|
58
|
+
self.model = model
|
|
59
|
+
self.temperature = temperature
|
|
60
|
+
self.max_tokens = max_tokens
|
|
61
|
+
self.parallel_ensemble = parallel_ensemble
|
|
62
|
+
self.api_key = api_key
|
|
63
|
+
self._a3m_router = None
|
|
64
|
+
self._initialized = False
|
|
65
|
+
|
|
66
|
+
def _ensure_router(self) -> None:
|
|
67
|
+
"""Lazily initialize the A3M router."""
|
|
68
|
+
if self._initialized:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
if not A3M_AVAILABLE:
|
|
72
|
+
raise ImportError(
|
|
73
|
+
"A3M Router is not installed. "
|
|
74
|
+
"Install with: pip install adaptive-memory-multi-model-router"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self._a3m_router = A3MRouter(
|
|
78
|
+
model=self.model,
|
|
79
|
+
temperature=self.temperature,
|
|
80
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
81
|
+
)
|
|
82
|
+
self._initialized = True
|
|
83
|
+
logger.info(
|
|
84
|
+
"A3M Router initialized: model=%s, ensemble=%d",
|
|
85
|
+
self.model,
|
|
86
|
+
self.parallel_ensemble,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def metadata(self) -> Dict[str, Any]:
|
|
91
|
+
"""Return LLM metadata as a dict (framework-agnostic)."""
|
|
92
|
+
return {
|
|
93
|
+
"context_window": 128000,
|
|
94
|
+
"num_output": self.max_tokens or 4096,
|
|
95
|
+
"model_name": self.model,
|
|
96
|
+
"is_chat_model": True,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def complete(self, prompt: str, **kwargs: Any) -> CompletionResponse:
|
|
100
|
+
"""Complete a prompt using A3M Router."""
|
|
101
|
+
self._ensure_router()
|
|
102
|
+
|
|
103
|
+
messages = [{"role": "user", "content": prompt}]
|
|
104
|
+
|
|
105
|
+
import asyncio
|
|
106
|
+
loop = asyncio.get_event_loop()
|
|
107
|
+
route_result = loop.run_in_executor(
|
|
108
|
+
None,
|
|
109
|
+
lambda: self._a3m_router.route(
|
|
110
|
+
messages=messages,
|
|
111
|
+
temperature=self.temperature,
|
|
112
|
+
max_tokens=self.max_tokens,
|
|
113
|
+
**kwargs,
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return CompletionResponse(text=route_result.content, raw=route_result)
|
|
118
|
+
|
|
119
|
+
def chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> CompletionResponse:
|
|
120
|
+
"""Chat completion using A3M Router."""
|
|
121
|
+
self._ensure_router()
|
|
122
|
+
|
|
123
|
+
a3m_messages = self._convert_messages(messages)
|
|
124
|
+
|
|
125
|
+
import asyncio
|
|
126
|
+
loop = asyncio.get_event_loop()
|
|
127
|
+
route_result = loop.run_in_executor(
|
|
128
|
+
None,
|
|
129
|
+
lambda: self._a3m_router.route(
|
|
130
|
+
messages=a3m_messages,
|
|
131
|
+
temperature=self.temperature,
|
|
132
|
+
max_tokens=self.max_tokens,
|
|
133
|
+
**kwargs,
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return CompletionResponse(text=route_result.content, raw=route_result)
|
|
138
|
+
|
|
139
|
+
def _convert_messages(self, messages: Sequence[ChatMessage]) -> List[Dict[str, Any]]:
|
|
140
|
+
"""Convert LlamaIndex ChatMessages to A3M format."""
|
|
141
|
+
a3m_messages = []
|
|
142
|
+
for msg in messages:
|
|
143
|
+
role = msg.role.value if hasattr(msg.role, 'value') else str(msg.role).lower()
|
|
144
|
+
role_map = {
|
|
145
|
+
"system": "system",
|
|
146
|
+
"user": "user",
|
|
147
|
+
"assistant": "assistant",
|
|
148
|
+
"tool": "tool",
|
|
149
|
+
"function": "function",
|
|
150
|
+
}
|
|
151
|
+
a3m_role = role_map.get(role, "user")
|
|
152
|
+
a3m_messages.append({"role": a3m_role, "content": msg.content})
|
|
153
|
+
return a3m_messages
|
|
154
|
+
|
|
155
|
+
def __repr__(self) -> str:
|
|
156
|
+
return (
|
|
157
|
+
f"A3MLlamaIndexAdapter("
|
|
158
|
+
f"model={self.model!r}, "
|
|
159
|
+
f"temperature={self.temperature}, "
|
|
160
|
+
f"max_tokens={self.max_tokens}, "
|
|
161
|
+
f"ensemble={self.parallel_ensemble})"
|
|
162
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for A3M adapters."""
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test script for A3M Router adapters.
|
|
3
|
+
|
|
4
|
+
Tests the LangChain and LlamaIndex adapters to ensure they:
|
|
5
|
+
1. Initialize correctly
|
|
6
|
+
2. Route requests properly
|
|
7
|
+
3. Return expected response types
|
|
8
|
+
4. Handle errors gracefully
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
# Add current directory to path
|
|
16
|
+
sys.path.insert(0, '.')
|
|
17
|
+
|
|
18
|
+
logging.basicConfig(level=logging.INFO)
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
def test_langchain_adapter():
|
|
22
|
+
"""Test LangChain adapter."""
|
|
23
|
+
print("Testing LangChain adapter...")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from a3m_llm_adapter import A3MChatModel
|
|
27
|
+
|
|
28
|
+
# Initialize
|
|
29
|
+
llm = A3MChatModel(model="auto", temperature=0.7)
|
|
30
|
+
print(f"��✅ Initialized: {llm}")
|
|
31
|
+
|
|
32
|
+
# Test simple generation
|
|
33
|
+
# Note: This would make actual API calls - we'll skip for now
|
|
34
|
+
# In a real test, we'd mock the A3M router
|
|
35
|
+
print("��✅ LangChain adapter structure OK")
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"��❌ LangChain adapter failed: {e}")
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
def test_llamaindex_adapter():
|
|
43
|
+
"""Test LlamaIndex adapter."""
|
|
44
|
+
print("Testing LlamaIndex adapter...")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
from a3m_llama_index_adapter import A3MLlamaIndexLLM
|
|
48
|
+
|
|
49
|
+
# Initialize
|
|
50
|
+
llm = A3MLlamaIndexLLM(model="auto", temperature=0.5)
|
|
51
|
+
print(f"��✅ Initialized: {llm}")
|
|
52
|
+
|
|
53
|
+
# Check metadata
|
|
54
|
+
metadata = llm.metadata
|
|
55
|
+
print(f"��✅ Metadata: {metadata.model_name}, tokens: {metadata.num_output}")
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"��❌ LlamaIndex adapter failed: {e}")
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def test_config():
|
|
63
|
+
"""Test configuration."""
|
|
64
|
+
print("Testing configuration...")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
from a3m_adapter_config import A3MConfig
|
|
68
|
+
|
|
69
|
+
# Test defaults
|
|
70
|
+
config = A3MConfig()
|
|
71
|
+
print(f"��✅ Default config: model={config.model}")
|
|
72
|
+
|
|
73
|
+
# Test to_dict
|
|
74
|
+
data = config.to_dict()
|
|
75
|
+
assert 'model' in data
|
|
76
|
+
print("��✅ Config to_dict works")
|
|
77
|
+
|
|
78
|
+
# Test JSON serialization
|
|
79
|
+
json_str = config.to_json()
|
|
80
|
+
assert '"model"' in json_str
|
|
81
|
+
print("��✅ Config JSON serialization works")
|
|
82
|
+
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"��❌ Config test failed: {e}")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
"""Run all tests."""
|
|
91
|
+
print("=" * 50)
|
|
92
|
+
print("A3M Router Adapter Tests")
|
|
93
|
+
print("=" * 50)
|
|
94
|
+
|
|
95
|
+
tests = [
|
|
96
|
+
test_config,
|
|
97
|
+
test_langchain_adapter,
|
|
98
|
+
test_llamaindex_adapter,
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
passed = 0
|
|
102
|
+
total = len(tests)
|
|
103
|
+
|
|
104
|
+
for test in tests:
|
|
105
|
+
if test():
|
|
106
|
+
passed += 1
|
|
107
|
+
print()
|
|
108
|
+
|
|
109
|
+
print("=" * 50)
|
|
110
|
+
print(f"Results: {passed}/{total} tests passed")
|
|
111
|
+
|
|
112
|
+
if passed == total:
|
|
113
|
+
print("���🎉 All tests passed!")
|
|
114
|
+
return 0
|
|
115
|
+
else:
|
|
116
|
+
print("��❌ Some tests failed")
|
|
117
|
+
return 1
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
sys.exit(main())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
setup(
|
|
5
|
+
name="a3m_adapter",
|
|
6
|
+
version="1.0.0",
|
|
7
|
+
description="A3M Router adapters for LangChain, LlamaIndex, and other LLM frameworks",
|
|
8
|
+
long_description=open("README.md").read() if os.path.exists("README.md") else "",
|
|
9
|
+
long_description_content_type="text/markdown",
|
|
10
|
+
author="A3M Team",
|
|
11
|
+
author_email="hello@a3m.ai",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
install_requires=[
|
|
14
|
+
"requests>=2.25.1",
|
|
15
|
+
"pydantic>=1.9.0",
|
|
16
|
+
],
|
|
17
|
+
extras_require={
|
|
18
|
+
"langchain": ["langchain>=0.0.365", "langchain-core>=0.0.365"],
|
|
19
|
+
"llamaindex": ["llama-index>=0.8.0"],
|
|
20
|
+
"dev": ["pytest>=6.0"],
|
|
21
|
+
},
|
|
22
|
+
python_requires=">=3.8",
|
|
23
|
+
)
|
|
@@ -67,12 +67,12 @@ Name: A3M Router
|
|
|
67
67
|
Tagline: #1 LLM Routing Benchmark — 130× cheaper than GPT-5
|
|
68
68
|
|
|
69
69
|
Description:
|
|
70
|
-
A3M Router is an open-source LLM routing proxy that ranks #1 on RouterArena
|
|
70
|
+
A3M Router is an open-source LLM routing proxy that ranks #1 on RouterArena
|
|
71
71
|
(arXiv:2510.00202) with a 0.9404 / 96.77% at $0.0768 per 1K queries — 130× cheaper
|
|
72
72
|
than GPT-5.
|
|
73
73
|
|
|
74
74
|
Key Features:
|
|
75
|
-
- #1 on RouterArena
|
|
75
|
+
- #1 on RouterArena benchmark (96.77%/19 routers)
|
|
76
76
|
- $0.0768/1K queries — 130× cheaper than GPT-5
|
|
77
77
|
- <1ms routing decision, no GPU required
|
|
78
78
|
- 47+ providers: OpenAI, Anthropic, Groq, Cerebras, DeepSeek, Gemini, Mistral
|