Skip to content

Commit 982c4d8

Browse files
author
Subho Mukherjee
committed
feat: Add framework adapters for LangChain and LlamaIndex
- A3MLangChainAdapter: Drop-in replacement for LangChain's ChatOpenAI - A3MLlamaIndexAdapter: Drop-in replacement for LlamaIndex's BaseLLM - A3MConfig: Configuration management for adapters Features: - Lazy initialization (no backend needed to instantiate) - Framework-agnostic design - Full compatibility with existing A3M Router backend Usage: from adapters import A3MLangChainAdapter, A3MLlamaIndexAdapter llm = A3MLangChainAdapter(model='auto', temperature=0.7) response = llm.invoke('What is the capital of France?')
1 parent eab9f73 commit 982c4d8

10 files changed

Lines changed: 644 additions & 0 deletions

File tree

‎adapters/README.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
```

‎adapters/__init__.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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'

‎adapters/a3m_adapter/__init__.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""A3M Router adapter implementations."""
2+
3+
from .langchain import A3MLangChainAdapter
4+
from .llamaindex import A3MLlamaIndexAdapter
5+
from .config import A3MConfig
6+
7+
__all__ = ['A3MLangChainAdapter', 'A3MLlamaIndexAdapter', 'A3MConfig']
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)