adaptive-memory-multi-model-router 2.15.3 → 2.15.5

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.
Files changed (101) hide show
  1. package/.github/workflows/adapters-ci.yml +142 -0
  2. package/.github/workflows/ci.yml +2 -5
  3. package/.github/workflows/pypi-publish.yml +102 -0
  4. package/CHANGELOG.md +7 -1
  5. package/README.md +260 -137
  6. package/README_ja.md +2 -2
  7. package/README_zh.md +1 -1
  8. package/adapters/README.md +36 -0
  9. package/adapters/__init__.py +25 -0
  10. package/adapters/a3m_adapter/__init__.py +51 -0
  11. package/adapters/a3m_adapter/adapter/__init__.py +22 -0
  12. package/adapters/a3m_adapter/adapter/autogen.py +169 -0
  13. package/adapters/a3m_adapter/adapter/config.py +100 -0
  14. package/adapters/a3m_adapter/adapter/haystack.py +197 -0
  15. package/adapters/a3m_adapter/adapter/langchain.py +155 -0
  16. package/adapters/a3m_adapter/adapter/langgraph.py +196 -0
  17. package/adapters/a3m_adapter/adapter/llamaindex.py +162 -0
  18. package/adapters/a3m_adapter/adapter/pinecone.py +217 -0
  19. package/adapters/a3m_adapter/adapter/vercel.py +188 -0
  20. package/adapters/a3m_adapter/tests/__init__.py +1 -0
  21. package/adapters/a3m_adapter/tests/test_adapters.py +118 -0
  22. package/adapters/a3m_adapter/tests/test_integration.py +80 -0
  23. package/adapters/requirements-dev.txt +6 -0
  24. package/adapters/requirements.txt +4 -0
  25. package/adapters/setup.py +23 -0
  26. package/apps/cost-calculator/README.md +72 -0
  27. package/apps/cost-calculator/calculator.css +280 -0
  28. package/apps/cost-calculator/calculator.js +150 -0
  29. package/apps/cost-calculator/index.html +321 -0
  30. package/apps/cost-calculator/package.json +13 -0
  31. package/articles/ANNOUNCEMENT_reddit_ml.md +76 -0
  32. package/articles/ANNOUNCEMENT_vc/347/244/276/345/214/272.md +71 -0
  33. package/articles/ANNOUNCEMENT_vercel.md +85 -0
  34. package/demo.py +251 -0
  35. package/dist/providers/providerConfig.d.ts +5 -1
  36. package/dist/providers/providerConfig.js +1006 -1
  37. package/dist/providers/providerConfig.js.map +1 -1
  38. package/docker-compose.yml +84 -60
  39. package/docs/comparison.md +2 -2
  40. package/docs/llms-full.txt +360 -138
  41. package/docs/llms.txt +70 -71
  42. package/docs-site/index.html +9 -9
  43. package/llms.txt +70 -62
  44. package/package.json +41 -91
  45. package/packages/a3m-vercel-ai/README.md +161 -0
  46. package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts +12 -0
  47. package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts.map +1 -0
  48. package/packages/a3m-vercel-ai/dist/a3m-language-model.js +289 -0
  49. package/packages/a3m-vercel-ai/dist/a3m-language-model.js.map +1 -0
  50. package/packages/a3m-vercel-ai/dist/index.d.ts +82 -0
  51. package/packages/a3m-vercel-ai/dist/index.d.ts.map +1 -0
  52. package/packages/a3m-vercel-ai/dist/index.js +79 -0
  53. package/packages/a3m-vercel-ai/dist/index.js.map +1 -0
  54. package/packages/a3m-vercel-ai/dist/types.d.ts +97 -0
  55. package/packages/a3m-vercel-ai/dist/types.d.ts.map +1 -0
  56. package/packages/a3m-vercel-ai/dist/types.js +5 -0
  57. package/packages/a3m-vercel-ai/dist/types.js.map +1 -0
  58. package/packages/a3m-vercel-ai/package-lock.json +969 -0
  59. package/packages/a3m-vercel-ai/package.json +46 -0
  60. package/packages/a3m-vercel-ai/src/a3m-language-model.ts +381 -0
  61. package/packages/a3m-vercel-ai/src/index.ts +104 -0
  62. package/packages/a3m-vercel-ai/src/types.ts +116 -0
  63. package/packages/a3m-vercel-ai/tsconfig.json +20 -0
  64. package/packages/agentkit-adapter/LICENSE +21 -0
  65. package/packages/agentkit-adapter/README.md +126 -0
  66. package/packages/agentkit-adapter/examples/agentkit-example.ts +139 -0
  67. package/packages/agentkit-adapter/package.json +57 -0
  68. package/packages/agentkit-adapter/src/adapter.ts +381 -0
  69. package/packages/agentkit-adapter/src/index.ts +36 -0
  70. package/packages/agentkit-adapter/src/types.ts +105 -0
  71. package/packages/agentkit-adapter/src/util.ts +13 -0
  72. package/packages/agentkit-adapter/tsconfig.json +22 -0
  73. package/prometheus.yml +8 -0
  74. package/python/README.md +92 -65
  75. package/python/a3m/__init__.py +32 -3
  76. package/python/a3m/adapters/__init__.py +21 -0
  77. package/python/a3m/adapters/langchain.py +190 -0
  78. package/python/a3m/adapters/llamaindex.py +249 -0
  79. package/python/a3m/adapters/qdrant.py +240 -0
  80. package/python/a3m/adapters/weaviate.py +263 -0
  81. package/python/a3m/client.py +5 -0
  82. package/python/a3m_router.egg-info/PKG-INFO +172 -0
  83. package/python/a3m_router.egg-info/SOURCES.txt +17 -0
  84. package/python/a3m_router.egg-info/dependency_links.txt +1 -0
  85. package/python/a3m_router.egg-info/requires.txt +24 -0
  86. package/python/a3m_router.egg-info/top_level.txt +1 -0
  87. package/python/build_verify.sh +32 -0
  88. package/python/dist/a3m_router-2.2.1-py3-none-any.whl +0 -0
  89. package/python/dist/a3m_router-2.2.1.tar.gz +0 -0
  90. package/python/dist/a3m_router-2.2.2-py3-none-any.whl +0 -0
  91. package/python/dist/a3m_router-2.2.2.tar.gz +0 -0
  92. package/python/mcp-server/README.md +172 -0
  93. package/python/mcp-server/a3m_mcp/__init__.py +15 -0
  94. package/python/mcp-server/a3m_mcp/__main__.py +15 -0
  95. package/python/mcp-server/a3m_mcp/server.py +339 -0
  96. package/python/mcp-server/pyproject.toml +24 -0
  97. package/python/pyproject.toml +56 -5
  98. package/python/setup.py +3 -28
  99. package/src/providers/providerConfig.ts +1053 -1
  100. package/summary.txt +38 -0
  101. package/tsconfig.build.json +1 -2
@@ -0,0 +1,188 @@
1
+ """
2
+ A3M Router Adapter for Vercel AI SDK.
3
+
4
+ Drop-in replacement for Vercel AI SDK's AI function that routes through A3M Router
5
+ for intelligent, cost-optimized responses in Next.js and other JavaScript environments.
6
+
7
+ Usage (JavaScript):
8
+ import { generateText } from 'ai';
9
+ import { createA3MProvider } from 'a3m-adapter/vercel';
10
+
11
+ const result = await generateText({
12
+ model: createA3MProvider({ model: 'auto', parallel_ensemble: 2 }),
13
+ prompt: 'What is the meaning of life?',
14
+ });
15
+
16
+ Usage (Python):
17
+ from a3m_adapter import A3MVercelAdapter
18
+
19
+ adapter = A3MVercelAdapter(model='auto', temperature=0.7)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ from typing import Any, Dict, List, Optional
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ A3M_AVAILABLE = False
30
+ try:
31
+ from a3m.router import A3MRouter, RouteResponse
32
+ A3M_AVAILABLE = True
33
+ except ImportError:
34
+ logger.warning(
35
+ "A3M Router not installed. Install with: pip install adaptive-memory-multi-model-router"
36
+ )
37
+
38
+
39
+ class A3MVercelAdapter:
40
+ """
41
+ A3M Router adapter for Vercel AI SDK compatibility.
42
+
43
+ Provides a drop-in replacement that routes through A3M Router
44
+ instead of calling OpenAI/Anthropic directly.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ model: str = "auto",
50
+ temperature: float = 0.7,
51
+ max_tokens: int = 4096,
52
+ parallel_ensemble: int = 1,
53
+ api_key: Optional[str] = None,
54
+ **kwargs: Any,
55
+ ) -> None:
56
+ """
57
+ Initialize A3M Router adapter for Vercel AI SDK.
58
+ """
59
+ self.model = model
60
+ self.temperature = temperature
61
+ self.max_tokens = max_tokens
62
+ self.parallel_ensemble = parallel_ensemble
63
+ self.api_key = api_key
64
+ self._a3m_router = None
65
+ self._initialized = False
66
+ self._kwargs = kwargs
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 for Vercel AI SDK: model=%s",
87
+ self.model,
88
+ )
89
+
90
+ def __call__(
91
+ self,
92
+ prompt: str,
93
+ **kwargs: Any,
94
+ ) -> Dict[str, Any]:
95
+ """
96
+ Generate text from prompt (Vercel AI SDK compatible interface).
97
+
98
+ Args:
99
+ prompt: The prompt string
100
+
101
+ Returns:
102
+ Dict with 'text', 'provider', 'usage', 'finishReason'
103
+ """
104
+ self._ensure_router()
105
+
106
+ messages = [{"role": "user", "content": prompt}]
107
+
108
+ import asyncio
109
+ loop = asyncio.get_event_loop()
110
+ route_result = loop.run_in_executor(
111
+ None,
112
+ lambda: self._a3m_router.route(
113
+ messages=messages,
114
+ temperature=kwargs.get("temperature", self.temperature),
115
+ max_tokens=kwargs.get("max_tokens", self.max_tokens),
116
+ **kwargs,
117
+ ),
118
+ )
119
+
120
+ return {
121
+ "text": route_result.content,
122
+ "provider": getattr(route_result, 'provider', 'a3m'),
123
+ "finishReason": getattr(route_result, 'finish_reason', 'stop'),
124
+ "usage": {
125
+ "promptTokens": getattr(route_result, 'prompt_tokens', 0),
126
+ "completionTokens": getattr(route_result, 'completion_tokens', 0),
127
+ "totalTokens": getattr(route_result, 'total_tokens', 0),
128
+ },
129
+ }
130
+
131
+ async def generate(
132
+ self,
133
+ prompt: str,
134
+ **kwargs: Any,
135
+ ) -> Dict[str, Any]:
136
+ """Async generate for Vercel AI SDK."""
137
+ self._ensure_router()
138
+
139
+ messages = [{"role": "user", "content": prompt}]
140
+
141
+ route_result = await self._a3m_router.aroute(
142
+ messages=messages,
143
+ temperature=kwargs.get("temperature", self.temperature),
144
+ max_tokens=kwargs.get("max_tokens", self.max_tokens),
145
+ **kwargs,
146
+ )
147
+
148
+ return {
149
+ "text": route_result.content,
150
+ "provider": getattr(route_result, 'provider', 'a3m'),
151
+ "finishReason": getattr(route_result, 'finish_reason', 'stop'),
152
+ "usage": {
153
+ "promptTokens": getattr(route_result, 'prompt_tokens', 0),
154
+ "completionTokens": getattr(route_result, 'completion_tokens', 0),
155
+ "totalTokens": getattr(route_result, 'total_tokens', 0),
156
+ },
157
+ }
158
+
159
+ def __repr__(self) -> str:
160
+ return (
161
+ f"A3MVercelAdapter("
162
+ f"model={self.model!r}, "
163
+ f"temperature={self.temperature}, "
164
+ f"max_tokens={self.max_tokens})"
165
+ )
166
+
167
+
168
+ # JavaScript-compatible factory function
169
+ def createA3MProvider(config: Dict[str, Any]) -> A3MVercelAdapter:
170
+ """
171
+ Create an A3M Provider for Vercel AI SDK (JavaScript usage).
172
+
173
+ Usage:
174
+ import { generateText } from 'ai';
175
+ import { createA3MProvider } from 'a3m-adapter/vercel';
176
+
177
+ const result = await generateText({
178
+ model: createA3MProvider({ model: 'auto', parallel_ensemble: 2 }),
179
+ prompt: 'What is 2+2?',
180
+ });
181
+ """
182
+ return A3MVercelAdapter(
183
+ model=config.get("model", "auto"),
184
+ temperature=config.get("temperature", 0.7),
185
+ max_tokens=config.get("max_tokens", 4096),
186
+ parallel_ensemble=config.get("parallel_ensemble", 1),
187
+ api_key=config.get("api_key"),
188
+ )
@@ -0,0 +1 @@
1
+ """Tests for A3M adapters."""
@@ -0,0 +1,118 @@
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
+
22
+ def test_langchain_adapter():
23
+ """Test LangChain adapter."""
24
+ print("Testing LangChain adapter...")
25
+
26
+ try:
27
+ from a3m_adapter import A3MLangChainAdapter
28
+
29
+ # Initialize
30
+ llm = A3MLangChainAdapter(model="auto", temperature=0.7)
31
+ print(f"✅ Initialized: {llm}")
32
+
33
+ # Test simple generation
34
+ # Note: This would make actual API calls - we'll skip for now
35
+ # In a real test, we'd mock the A3M router
36
+ print("✅ LangChain adapter structure OK")
37
+ assert llm is not None
38
+
39
+ except Exception as e:
40
+ print(f"❌ LangChain adapter failed: {e}")
41
+ raise
42
+
43
+
44
+ def test_llamaindex_adapter():
45
+ """Test LlamaIndex adapter."""
46
+ print("Testing LlamaIndex adapter...")
47
+
48
+ try:
49
+ from a3m_adapter import A3MLlamaIndexAdapter
50
+
51
+ # Initialize
52
+ llm = A3MLlamaIndexAdapter(model="auto", temperature=0.5)
53
+ print(f"✅ Initialized: {llm}")
54
+
55
+ # Check metadata
56
+ metadata = llm.metadata
57
+ print(f"✅ Metadata: {metadata}")
58
+ assert metadata is not None
59
+
60
+ except Exception as e:
61
+ print(f"❌ LlamaIndex adapter failed: {e}")
62
+ raise
63
+
64
+
65
+ def test_config():
66
+ """Test configuration."""
67
+ print("Testing configuration...")
68
+
69
+ try:
70
+ from a3m_adapter import A3MConfig
71
+
72
+ # Test defaults
73
+ config = A3MConfig()
74
+ print(f"✅ Default config: model={config.model}")
75
+
76
+ # Test to_dict
77
+ data = config.to_dict()
78
+ assert 'model' in data
79
+ print("✅ Config to_dict works")
80
+
81
+ # Test JSON serialization
82
+ json_str = config.to_json()
83
+ assert '"model"' in json_str
84
+ print("✅ Config JSON serialization works")
85
+
86
+ except Exception as e:
87
+ print(f"❌ Config test failed: {e}")
88
+ raise
89
+
90
+
91
+ def main():
92
+ """Run all tests."""
93
+ print("=" * 50)
94
+ print("A3M Router Adapter Tests")
95
+ print("=" * 50)
96
+
97
+ tests = [
98
+ test_config,
99
+ test_langchain_adapter,
100
+ test_llamaindex_adapter,
101
+ ]
102
+
103
+ passed = 0
104
+ total = len(tests)
105
+
106
+ for test in tests:
107
+ test()
108
+ passed += 1
109
+ print()
110
+
111
+ print("=" * 50)
112
+ print(f"Results: {passed}/{total} tests passed")
113
+ print("🎉 All tests passed!")
114
+ return 0
115
+
116
+
117
+ if __name__ == "__main__":
118
+ sys.exit(main())
@@ -0,0 +1,80 @@
1
+ """
2
+ Integration tests for A3M Router adapters.
3
+ Requires A3M Router server running on localhost:8787
4
+ """
5
+
6
+ import pytest
7
+ import os
8
+
9
+
10
+ @pytest.fixture
11
+ def a3m_server_url():
12
+ """Get A3M Router server URL."""
13
+ return os.environ.get("A3M_SERVER_URL", "http://localhost:8787")
14
+
15
+
16
+ @pytest.fixture
17
+ def skip_if_no_server():
18
+ """Skip test if server is not available."""
19
+ import requests
20
+ try:
21
+ resp = requests.get("http://localhost:8787/health", timeout=2)
22
+ if resp.status_code != 200:
23
+ pytest.skip("A3M Router server not running")
24
+ except:
25
+ pytest.skip("A3M Router server not running")
26
+
27
+
28
+ @pytest.mark.integration
29
+ def test_simple_chat_completion(a3m_server_url, skip_if_no_server):
30
+ """Test simple chat completion via HTTP API."""
31
+ import requests
32
+
33
+ response = requests.post(
34
+ f"{a3m_server_url}/v1/chat/completions",
35
+ json={
36
+ "model": "auto",
37
+ "messages": [{"role": "user", "content": "What is 2+2?"}]
38
+ },
39
+ timeout=30
40
+ )
41
+
42
+ assert response.status_code == 200
43
+ data = response.json()
44
+ assert "choices" in data
45
+ assert len(data["choices"]) > 0
46
+ assert "message" in data["choices"][0]
47
+ assert data["choices"][0]["message"]["content"]
48
+
49
+
50
+ @pytest.mark.integration
51
+ def test_parallel_ensemble(a3m_server_url, skip_if_no_server):
52
+ """Test parallel ensemble with multiple providers."""
53
+ import requests
54
+
55
+ response = requests.post(
56
+ f"{a3m_server_url}/v1/chat/completions",
57
+ json={
58
+ "model": "auto",
59
+ "messages": [{"role": "user", "content": "Explain gravity"}],
60
+ "parallel_ensemble": 3,
61
+ },
62
+ timeout=60
63
+ )
64
+
65
+ assert response.status_code == 200
66
+ data = response.json()
67
+ assert "choices" in data
68
+ assert "provider" in data
69
+
70
+
71
+ @pytest.mark.integration
72
+ def test_health_endpoint(a3m_server_url, skip_if_no_server):
73
+ """Test health endpoint."""
74
+ import requests
75
+
76
+ response = requests.get(f"{a3m_server_url}/health", timeout=10)
77
+
78
+ assert response.status_code == 200
79
+ data = response.json()
80
+ assert "providers" in data or "status" in data
@@ -0,0 +1,6 @@
1
+ -r requirements.txt
2
+ black>=21.12
3
+ isort>=5.10.1
4
+ flake8>=3.9.0
5
+ build>=0.10.0
6
+ twine>=4.0.0
@@ -0,0 +1,4 @@
1
+ requests>=2.25.1
2
+ pydantic>=1.9.0
3
+ pytest>=6.0
4
+ pytest-asyncio>=0.21.0
@@ -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
+ )
@@ -0,0 +1,72 @@
1
+ # A3M Router Cost Calculator
2
+
3
+ **Live:** https://a3m.router/calculator
4
+
5
+ Drop-in interactive calculator. No build step. No npm dependencies for the calculator itself.
6
+
7
+ ## What it does
8
+
9
+ - Input: monthly request volume + model mix
10
+ - Output: side-by-side cost comparison (baseline vs A3M routed)
11
+ - Generates shareable URL with encoded config (base64url)
12
+ - Exports comparison as CSV
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ # Open directly
18
+ open apps/cost-calculator/index.html
19
+
20
+ # Or serve locally
21
+ npx serve apps/cost-calculator
22
+ ```
23
+
24
+ ## Embed in any site
25
+
26
+ ```html
27
+ <iframe
28
+ src="https://a3m.router/calculator"
29
+ width="100%"
30
+ height="700"
31
+ frameborder="0"
32
+ style="border-radius: 12px;"
33
+ ></iframe>
34
+ ```
35
+
36
+ ## Architecture
37
+
38
+ ```
39
+ calculator.js — Pure JS, no dependencies. All pricing, routing, and encoding logic.
40
+ calculator.css — Standalone styles. Dark theme, responsive.
41
+ index.html — Self-contained. Vanilla JS. Loads calculator.js.
42
+ package.json — vitest for unit testing the pure JS logic.
43
+ ```
44
+
45
+ ## Testing
46
+
47
+ ```bash
48
+ cd apps/cost-calculator
49
+ npm install
50
+ npm test
51
+ ```
52
+
53
+ ## Models Supported
54
+
55
+ | Premium Model | Routed To | Savings |
56
+ |---------------|-----------|---------|
57
+ | GPT-4o | GPT-4o-mini | ~35% |
58
+ | Claude 3.5 Sonnet | Claude 3 Haiku | ~40% |
59
+ | Gemini 1.5 Pro | Gemini 1.5 Flash | ~45% |
60
+ | Groq Llama 3.3 70B | Groq Mixtral 8x7B | ~30% |
61
+
62
+ ## Pricing Source
63
+
64
+ OpenRouter provider rates (per 1M tokens). Update `PRICING` constant in `calculator.js` to reflect latest rates.
65
+
66
+ ## Next Steps
67
+
68
+ - [ ] Deploy to `a3m.router/calculator` (Vercel static)
69
+ - [ ] Add UTM tracking on share links
70
+ - [ ] Add "embed widget" script for dynamic resize
71
+ - [ ] Write HN + Reddit post with actual example calculations
72
+ - [ ] Add unit tests with vitest