lighthouse-llm-registry 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lighthouse_llm_registry-0.1.0/.gitignore +3 -0
- lighthouse_llm_registry-0.1.0/.vscode/settings.json +3 -0
- lighthouse_llm_registry-0.1.0/LICENSE +21 -0
- lighthouse_llm_registry-0.1.0/PKG-INFO +182 -0
- lighthouse_llm_registry-0.1.0/README.md +147 -0
- lighthouse_llm_registry-0.1.0/pyproject.toml +45 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/__init__.py +28 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/base.py +9 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/config.py +9 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/errors.py +132 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/providers/__init__.py +1 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/providers/anthropic.py +20 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/providers/google.py +20 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/providers/groq.py +20 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/providers/local_llm.py +37 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/providers/openai.py +20 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/registry.py +143 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/result.py +13 -0
- lighthouse_llm_registry-0.1.0/src/llm_registry/tools/__init__.py +0 -0
- lighthouse_llm_registry-0.1.0/tests/chat_test.py +115 -0
- lighthouse_llm_registry-0.1.0/tests/local_llm.py +16 -0
- lighthouse_llm_registry-0.1.0/tests/test_groq.py +59 -0
- lighthouse_llm_registry-0.1.0/tests/test_local_env.py +57 -0
- lighthouse_llm_registry-0.1.0/tests/test_openai.py +61 -0
- lighthouse_llm_registry-0.1.0/tests/test_registry.py +136 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rohit Jagtap
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lighthouse-llm-registry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A global reusable LLM Registry wrapper for LangChain models with unified error mapping for Lighthouse Agents Factory
|
|
5
|
+
Author-email: Rohit Jagtap <rohit.jagtap@lighthouseindia.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: ai,anthropic,gemini,groq,langchain,llm,openai,registry
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: langchain-anthropic; extra == 'all'
|
|
23
|
+
Requires-Dist: langchain-google-genai; extra == 'all'
|
|
24
|
+
Requires-Dist: langchain-groq; extra == 'all'
|
|
25
|
+
Requires-Dist: langchain-openai; extra == 'all'
|
|
26
|
+
Provides-Extra: anthropic
|
|
27
|
+
Requires-Dist: langchain-anthropic; extra == 'anthropic'
|
|
28
|
+
Provides-Extra: google
|
|
29
|
+
Requires-Dist: langchain-google-genai; extra == 'google'
|
|
30
|
+
Provides-Extra: groq
|
|
31
|
+
Requires-Dist: langchain-groq; extra == 'groq'
|
|
32
|
+
Provides-Extra: openai
|
|
33
|
+
Requires-Dist: langchain-openai; extra == 'openai'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# LLM Registry
|
|
37
|
+
|
|
38
|
+
A global, reusable LLM Registry wrapper for LangChain chat models. It enables unified model configuration management, lazy loading, and dynamic resolution of model providers (OpenAI, Groq, Anthropic, and Google Gemini).
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation & Setup
|
|
43
|
+
|
|
44
|
+
> [!NOTE]
|
|
45
|
+
> The distribution package name is **`lighthouse-llm-registry`** (hyphen), and the Python import package name is **`llm_registry`** (underscore).
|
|
46
|
+
|
|
47
|
+
### 1. Installation
|
|
48
|
+
|
|
49
|
+
Install the core package:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install lighthouse-llm-registry
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
To install specific provider SDK dependencies:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# OpenAI support
|
|
59
|
+
pip install "lighthouse-llm-registry[openai]"
|
|
60
|
+
|
|
61
|
+
# Groq support
|
|
62
|
+
pip install "lighthouse-llm-registry[groq]"
|
|
63
|
+
|
|
64
|
+
# Anthropic support
|
|
65
|
+
pip install "lighthouse-llm-registry[anthropic]"
|
|
66
|
+
|
|
67
|
+
# Google Gemini support
|
|
68
|
+
pip install "lighthouse-llm-registry[google]"
|
|
69
|
+
|
|
70
|
+
# Install all supported providers
|
|
71
|
+
pip install "lighthouse-llm-registry[all]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If installing from local source distribution:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install ".[all]"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 2. API Keys Configuration
|
|
81
|
+
|
|
82
|
+
Create a `.env` file in the root of your target project:
|
|
83
|
+
|
|
84
|
+
```env
|
|
85
|
+
OPENAI_API_KEY=your-openai-key
|
|
86
|
+
GROQ_API_KEY=your-groq-key
|
|
87
|
+
ANTHROPIC_API_KEY=your-anthropic-key
|
|
88
|
+
GOOGLE_API_KEY=your-gemini-key
|
|
89
|
+
LOCAL_MODEL_LINK=http://192.168.101.88:11434/api/chat
|
|
90
|
+
LOCAL_MODEL=llama3.1:8b
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Usage Guide
|
|
96
|
+
|
|
97
|
+
### 1. Registration (Application Startup)
|
|
98
|
+
|
|
99
|
+
Register your model configurations once at application startup (e.g., in your app's main entry point):
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# main.py
|
|
103
|
+
from dotenv import load_dotenv
|
|
104
|
+
from llm_registry import global_registry, ModelConfig
|
|
105
|
+
|
|
106
|
+
# Load environment keys from .env
|
|
107
|
+
load_dotenv()
|
|
108
|
+
|
|
109
|
+
# Register OpenAI config (defaults to standard temperature/max_tokens rules)
|
|
110
|
+
global_registry.register_model_config(
|
|
111
|
+
"primary-chat",
|
|
112
|
+
ModelConfig(
|
|
113
|
+
provider="openai",
|
|
114
|
+
model_name="gpt-4o-mini",
|
|
115
|
+
temperature=0.7
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Register Groq config
|
|
120
|
+
global_registry.register_model_config(
|
|
121
|
+
"fast-chat",
|
|
122
|
+
ModelConfig(
|
|
123
|
+
provider="groq",
|
|
124
|
+
model_name="llama-3.1-8b-instant",
|
|
125
|
+
temperature=0.2
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Register Anthropic config (supports models like claude-opus-4-8)
|
|
130
|
+
global_registry.register_model_config(
|
|
131
|
+
"legacy-chat",
|
|
132
|
+
ModelConfig(
|
|
133
|
+
provider="anthropic",
|
|
134
|
+
model_name="claude-opus-4-8"
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Register Local Ollama config (falls back to LOCAL_MODEL_LINK and LOCAL_MODEL if parameters are omitted)
|
|
139
|
+
global_registry.register_model_config(
|
|
140
|
+
"local-chat",
|
|
141
|
+
ModelConfig(
|
|
142
|
+
provider="local",
|
|
143
|
+
model_name=""
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 2. Resolution (Anywhere in your code)
|
|
149
|
+
|
|
150
|
+
Resolve and query the model anywhere inside your application without importing specific provider SDKs:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# agents/agent.py
|
|
154
|
+
from llm_registry import global_registry
|
|
155
|
+
|
|
156
|
+
class SimpleAgent:
|
|
157
|
+
def __init__(self):
|
|
158
|
+
# Dynamically resolve model from the global registry
|
|
159
|
+
self.model = global_registry.get_model("fast-chat")
|
|
160
|
+
|
|
161
|
+
def respond(self, query: str) -> str:
|
|
162
|
+
# Standard LangChain invoke call
|
|
163
|
+
response = self.model.invoke(query)
|
|
164
|
+
return response.content
|
|
165
|
+
|
|
166
|
+
# Example for Local Ollama Model:
|
|
167
|
+
local_model = global_registry.get_model("local-chat")
|
|
168
|
+
local_response = local_model.invoke([
|
|
169
|
+
{"role": "user", "content": "How are you today?"}
|
|
170
|
+
])
|
|
171
|
+
print(local_response) # Returns direct text output
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Running Playground tests
|
|
177
|
+
|
|
178
|
+
An interactive chat test script is included under `tests/chat_test.py` to check your connections. Run it directly:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
python tests/chat_test.py
|
|
182
|
+
```
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# LLM Registry
|
|
2
|
+
|
|
3
|
+
A global, reusable LLM Registry wrapper for LangChain chat models. It enables unified model configuration management, lazy loading, and dynamic resolution of model providers (OpenAI, Groq, Anthropic, and Google Gemini).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation & Setup
|
|
8
|
+
|
|
9
|
+
> [!NOTE]
|
|
10
|
+
> The distribution package name is **`lighthouse-llm-registry`** (hyphen), and the Python import package name is **`llm_registry`** (underscore).
|
|
11
|
+
|
|
12
|
+
### 1. Installation
|
|
13
|
+
|
|
14
|
+
Install the core package:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install lighthouse-llm-registry
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
To install specific provider SDK dependencies:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# OpenAI support
|
|
24
|
+
pip install "lighthouse-llm-registry[openai]"
|
|
25
|
+
|
|
26
|
+
# Groq support
|
|
27
|
+
pip install "lighthouse-llm-registry[groq]"
|
|
28
|
+
|
|
29
|
+
# Anthropic support
|
|
30
|
+
pip install "lighthouse-llm-registry[anthropic]"
|
|
31
|
+
|
|
32
|
+
# Google Gemini support
|
|
33
|
+
pip install "lighthouse-llm-registry[google]"
|
|
34
|
+
|
|
35
|
+
# Install all supported providers
|
|
36
|
+
pip install "lighthouse-llm-registry[all]"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If installing from local source distribution:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install ".[all]"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. API Keys Configuration
|
|
46
|
+
|
|
47
|
+
Create a `.env` file in the root of your target project:
|
|
48
|
+
|
|
49
|
+
```env
|
|
50
|
+
OPENAI_API_KEY=your-openai-key
|
|
51
|
+
GROQ_API_KEY=your-groq-key
|
|
52
|
+
ANTHROPIC_API_KEY=your-anthropic-key
|
|
53
|
+
GOOGLE_API_KEY=your-gemini-key
|
|
54
|
+
LOCAL_MODEL_LINK=http://192.168.101.88:11434/api/chat
|
|
55
|
+
LOCAL_MODEL=llama3.1:8b
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Usage Guide
|
|
61
|
+
|
|
62
|
+
### 1. Registration (Application Startup)
|
|
63
|
+
|
|
64
|
+
Register your model configurations once at application startup (e.g., in your app's main entry point):
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# main.py
|
|
68
|
+
from dotenv import load_dotenv
|
|
69
|
+
from llm_registry import global_registry, ModelConfig
|
|
70
|
+
|
|
71
|
+
# Load environment keys from .env
|
|
72
|
+
load_dotenv()
|
|
73
|
+
|
|
74
|
+
# Register OpenAI config (defaults to standard temperature/max_tokens rules)
|
|
75
|
+
global_registry.register_model_config(
|
|
76
|
+
"primary-chat",
|
|
77
|
+
ModelConfig(
|
|
78
|
+
provider="openai",
|
|
79
|
+
model_name="gpt-4o-mini",
|
|
80
|
+
temperature=0.7
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Register Groq config
|
|
85
|
+
global_registry.register_model_config(
|
|
86
|
+
"fast-chat",
|
|
87
|
+
ModelConfig(
|
|
88
|
+
provider="groq",
|
|
89
|
+
model_name="llama-3.1-8b-instant",
|
|
90
|
+
temperature=0.2
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Register Anthropic config (supports models like claude-opus-4-8)
|
|
95
|
+
global_registry.register_model_config(
|
|
96
|
+
"legacy-chat",
|
|
97
|
+
ModelConfig(
|
|
98
|
+
provider="anthropic",
|
|
99
|
+
model_name="claude-opus-4-8"
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Register Local Ollama config (falls back to LOCAL_MODEL_LINK and LOCAL_MODEL if parameters are omitted)
|
|
104
|
+
global_registry.register_model_config(
|
|
105
|
+
"local-chat",
|
|
106
|
+
ModelConfig(
|
|
107
|
+
provider="local",
|
|
108
|
+
model_name=""
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 2. Resolution (Anywhere in your code)
|
|
114
|
+
|
|
115
|
+
Resolve and query the model anywhere inside your application without importing specific provider SDKs:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# agents/agent.py
|
|
119
|
+
from llm_registry import global_registry
|
|
120
|
+
|
|
121
|
+
class SimpleAgent:
|
|
122
|
+
def __init__(self):
|
|
123
|
+
# Dynamically resolve model from the global registry
|
|
124
|
+
self.model = global_registry.get_model("fast-chat")
|
|
125
|
+
|
|
126
|
+
def respond(self, query: str) -> str:
|
|
127
|
+
# Standard LangChain invoke call
|
|
128
|
+
response = self.model.invoke(query)
|
|
129
|
+
return response.content
|
|
130
|
+
|
|
131
|
+
# Example for Local Ollama Model:
|
|
132
|
+
local_model = global_registry.get_model("local-chat")
|
|
133
|
+
local_response = local_model.invoke([
|
|
134
|
+
{"role": "user", "content": "How are you today?"}
|
|
135
|
+
])
|
|
136
|
+
print(local_response) # Returns direct text output
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Running Playground tests
|
|
142
|
+
|
|
143
|
+
An interactive chat test script is included under `tests/chat_test.py` to check your connections. Run it directly:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
python tests/chat_test.py
|
|
147
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lighthouse-llm-registry"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A global reusable LLM Registry wrapper for LangChain models with unified error mapping for Lighthouse Agents Factory"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Rohit Jagtap", email = "rohit.jagtap@lighthouseindia.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["llm", "langchain", "registry", "ai", "openai", "groq", "anthropic", "gemini"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"langchain-core>=0.3.0",
|
|
29
|
+
"pydantic>=2.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
openai = ["langchain-openai"]
|
|
34
|
+
anthropic = ["langchain-anthropic"]
|
|
35
|
+
google = ["langchain-google-genai"]
|
|
36
|
+
groq = ["langchain-groq"]
|
|
37
|
+
all = [
|
|
38
|
+
"langchain-openai",
|
|
39
|
+
"langchain-anthropic",
|
|
40
|
+
"langchain-google-genai",
|
|
41
|
+
"langchain-groq"
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/llm_registry"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from llm_registry.registry import global_registry, LLMRegistry
|
|
2
|
+
from llm_registry.config import ModelConfig
|
|
3
|
+
from llm_registry.base import BaseLLMProvider
|
|
4
|
+
from llm_registry.errors import (
|
|
5
|
+
LLMErrorDetails,
|
|
6
|
+
LLMInvocationError,
|
|
7
|
+
LLMRegistryError,
|
|
8
|
+
ModelAliasNotFoundError,
|
|
9
|
+
ModelCreationError,
|
|
10
|
+
ProviderDependencyError,
|
|
11
|
+
ProviderNotRegisteredError,
|
|
12
|
+
)
|
|
13
|
+
from llm_registry.result import LLMResult
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"global_registry",
|
|
17
|
+
"LLMRegistry",
|
|
18
|
+
"ModelConfig",
|
|
19
|
+
"BaseLLMProvider",
|
|
20
|
+
"LLMErrorDetails",
|
|
21
|
+
"LLMInvocationError",
|
|
22
|
+
"LLMRegistryError",
|
|
23
|
+
"ModelAliasNotFoundError",
|
|
24
|
+
"ModelCreationError",
|
|
25
|
+
"ProviderDependencyError",
|
|
26
|
+
"ProviderNotRegisteredError",
|
|
27
|
+
"LLMResult",
|
|
28
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from langchain_core.language_models import BaseChatModel
|
|
3
|
+
from llm_registry.config import ModelConfig
|
|
4
|
+
|
|
5
|
+
class BaseLLMProvider(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def create_model(self, config: ModelConfig) -> BaseChatModel:
|
|
8
|
+
"""Instantiate and return a LangChain ChatModel based on the ModelConfig."""
|
|
9
|
+
pass
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
class ModelConfig(BaseModel):
|
|
5
|
+
provider: str = Field(..., description="The LLM provider, e.g., 'openai', 'anthropic', 'google', 'groq'")
|
|
6
|
+
model_name: str = Field(..., description="The name of the model, e.g., 'gpt-4o', 'llama-3.1-8b-instant'")
|
|
7
|
+
temperature: Optional[float] = Field(None, ge=0.0, le=2.0, description="Temperature for sampling (optional)")
|
|
8
|
+
max_tokens: Optional[int] = Field(None, description="Max tokens to generate (optional)")
|
|
9
|
+
extra_params: Dict[str, Any] = Field(default_factory=dict, description="Any extra provider-specific parameter kwargs")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class LLMErrorDetails:
|
|
7
|
+
"""Structured error details for provider and registry failures."""
|
|
8
|
+
|
|
9
|
+
provider: Optional[str]
|
|
10
|
+
model_name: Optional[str]
|
|
11
|
+
category: str
|
|
12
|
+
message: str
|
|
13
|
+
exception_type: str
|
|
14
|
+
status_code: Optional[int] = None
|
|
15
|
+
code: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LLMRegistryError(Exception):
|
|
19
|
+
"""Base exception for all llm_registry errors."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, details: Optional[LLMErrorDetails] = None):
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.details = details
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ModelAliasNotFoundError(LLMRegistryError, ValueError):
|
|
27
|
+
"""Raised when an alias has not been registered."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProviderNotRegisteredError(LLMRegistryError, ValueError):
|
|
31
|
+
"""Raised when no provider implementation exists for a model config."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ProviderDependencyError(LLMRegistryError, ImportError):
|
|
35
|
+
"""Raised when an optional provider dependency is not installed."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ModelCreationError(LLMRegistryError):
|
|
39
|
+
"""Raised when a provider cannot create a model instance."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LLMInvocationError(LLMRegistryError):
|
|
43
|
+
"""Raised when a provider API call fails."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalize_exception(
|
|
47
|
+
exc: Exception,
|
|
48
|
+
*,
|
|
49
|
+
provider: Optional[str] = None,
|
|
50
|
+
model_name: Optional[str] = None,
|
|
51
|
+
) -> LLMErrorDetails:
|
|
52
|
+
"""Map provider-specific exceptions into stable package-level details."""
|
|
53
|
+
|
|
54
|
+
exception_type = exc.__class__.__name__
|
|
55
|
+
message = str(exc) or exception_type
|
|
56
|
+
status_code = _get_status_code(exc)
|
|
57
|
+
code = _get_code(exc)
|
|
58
|
+
searchable = f"{exception_type} {message} {code or ''}".lower()
|
|
59
|
+
|
|
60
|
+
if status_code in {401, 403} or _contains_any(
|
|
61
|
+
searchable,
|
|
62
|
+
("authentication", "permissiondenied", "unauthorized", "invalid api key", "api key"),
|
|
63
|
+
):
|
|
64
|
+
category = "authentication"
|
|
65
|
+
elif status_code == 429 or _contains_any(searchable, ("ratelimit", "rate limit", "too many requests")):
|
|
66
|
+
category = "rate_limit"
|
|
67
|
+
elif _contains_any(searchable, ("quota", "insufficient_quota", "billing")):
|
|
68
|
+
category = "quota"
|
|
69
|
+
elif status_code in {408, 504} or _contains_any(searchable, ("timeout", "deadlineexceeded", "deadline exceeded")):
|
|
70
|
+
category = "timeout"
|
|
71
|
+
elif status_code in {400, 404, 422} or _contains_any(
|
|
72
|
+
searchable,
|
|
73
|
+
("badrequest", "invalidrequest", "validation", "notfound", "not found"),
|
|
74
|
+
):
|
|
75
|
+
category = "bad_request"
|
|
76
|
+
elif _contains_any(searchable, ("context_length", "maximum context", "token limit", "too many tokens")):
|
|
77
|
+
category = "context_length"
|
|
78
|
+
elif status_code and status_code >= 500:
|
|
79
|
+
category = "provider_server"
|
|
80
|
+
elif _contains_any(
|
|
81
|
+
searchable,
|
|
82
|
+
("connection", "connecterror", "api connection", "service unavailable", "temporarily unavailable"),
|
|
83
|
+
):
|
|
84
|
+
category = "connection"
|
|
85
|
+
else:
|
|
86
|
+
category = "unknown"
|
|
87
|
+
|
|
88
|
+
return LLMErrorDetails(
|
|
89
|
+
provider=provider,
|
|
90
|
+
model_name=model_name,
|
|
91
|
+
category=category,
|
|
92
|
+
message=message,
|
|
93
|
+
exception_type=exception_type,
|
|
94
|
+
status_code=status_code,
|
|
95
|
+
code=code,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _contains_any(value: str, needles: tuple[str, ...]) -> bool:
|
|
100
|
+
return any(needle in value for needle in needles)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _get_status_code(exc: Exception) -> Optional[int]:
|
|
104
|
+
for attr in ("status_code", "status", "http_status", "http_status_code"):
|
|
105
|
+
value = getattr(exc, attr, None)
|
|
106
|
+
if isinstance(value, int):
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
response = getattr(exc, "response", None)
|
|
110
|
+
value = getattr(response, "status_code", None)
|
|
111
|
+
if isinstance(value, int):
|
|
112
|
+
return value
|
|
113
|
+
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _get_code(exc: Exception) -> Optional[str]:
|
|
118
|
+
for attr in ("code", "error_code", "type"):
|
|
119
|
+
value = getattr(exc, attr, None)
|
|
120
|
+
if value is not None:
|
|
121
|
+
return str(value)
|
|
122
|
+
|
|
123
|
+
body = getattr(exc, "body", None)
|
|
124
|
+
if isinstance(body, dict):
|
|
125
|
+
error = body.get("error", body)
|
|
126
|
+
if isinstance(error, dict):
|
|
127
|
+
for key in ("code", "type"):
|
|
128
|
+
value = error.get(key)
|
|
129
|
+
if value is not None:
|
|
130
|
+
return str(value)
|
|
131
|
+
|
|
132
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Providers package initialization.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from langchain_core.language_models import BaseChatModel
|
|
2
|
+
from llm_registry.base import BaseLLMProvider
|
|
3
|
+
from llm_registry.config import ModelConfig
|
|
4
|
+
|
|
5
|
+
class AnthropicProvider(BaseLLMProvider):
|
|
6
|
+
def create_model(self, config: ModelConfig) -> BaseChatModel:
|
|
7
|
+
from langchain_anthropic import ChatAnthropic
|
|
8
|
+
|
|
9
|
+
kwargs = {
|
|
10
|
+
"model": config.model_name,
|
|
11
|
+
}
|
|
12
|
+
if config.temperature is not None:
|
|
13
|
+
kwargs["temperature"] = config.temperature
|
|
14
|
+
if config.max_tokens is not None:
|
|
15
|
+
kwargs["max_tokens"] = config.max_tokens
|
|
16
|
+
|
|
17
|
+
return ChatAnthropic(
|
|
18
|
+
**kwargs,
|
|
19
|
+
**config.extra_params
|
|
20
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from langchain_core.language_models import BaseChatModel
|
|
2
|
+
from llm_registry.base import BaseLLMProvider
|
|
3
|
+
from llm_registry.config import ModelConfig
|
|
4
|
+
|
|
5
|
+
class GoogleProvider(BaseLLMProvider):
|
|
6
|
+
def create_model(self, config: ModelConfig) -> BaseChatModel:
|
|
7
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
8
|
+
|
|
9
|
+
kwargs = {
|
|
10
|
+
"model": config.model_name,
|
|
11
|
+
}
|
|
12
|
+
if config.temperature is not None:
|
|
13
|
+
kwargs["temperature"] = config.temperature
|
|
14
|
+
if config.max_tokens is not None:
|
|
15
|
+
kwargs["max_tokens"] = config.max_tokens
|
|
16
|
+
|
|
17
|
+
return ChatGoogleGenerativeAI(
|
|
18
|
+
**kwargs,
|
|
19
|
+
**config.extra_params
|
|
20
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from langchain_core.language_models import BaseChatModel
|
|
2
|
+
from llm_registry.base import BaseLLMProvider
|
|
3
|
+
from llm_registry.config import ModelConfig
|
|
4
|
+
|
|
5
|
+
class GroqProvider(BaseLLMProvider):
|
|
6
|
+
def create_model(self, config: ModelConfig) -> BaseChatModel:
|
|
7
|
+
from langchain_groq import ChatGroq
|
|
8
|
+
|
|
9
|
+
kwargs = {
|
|
10
|
+
"model_name": config.model_name,
|
|
11
|
+
}
|
|
12
|
+
if config.temperature is not None:
|
|
13
|
+
kwargs["temperature"] = config.temperature
|
|
14
|
+
if config.max_tokens is not None:
|
|
15
|
+
kwargs["max_tokens"] = config.max_tokens
|
|
16
|
+
|
|
17
|
+
return ChatGroq(
|
|
18
|
+
**kwargs,
|
|
19
|
+
**config.extra_params
|
|
20
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
from llm_registry.base import BaseLLMProvider
|
|
4
|
+
from llm_registry.config import ModelConfig
|
|
5
|
+
|
|
6
|
+
class LocalLLM:
|
|
7
|
+
def __init__(self, model=None, api_url=None):
|
|
8
|
+
# Fall back to environment variables if parameters are not explicitly passed
|
|
9
|
+
self.api_url = api_url or os.getenv("LOCAL_MODEL_LINK")
|
|
10
|
+
self.model = model or os.getenv("LOCAL_MODEL")
|
|
11
|
+
|
|
12
|
+
if not self.api_url:
|
|
13
|
+
raise ValueError("LOCAL_MODEL_LINK not set in environment.")
|
|
14
|
+
if not self.model:
|
|
15
|
+
raise ValueError("LOCAL_MODEL not set in environment.")
|
|
16
|
+
|
|
17
|
+
def invoke(self, messages, **kwargs):
|
|
18
|
+
payload = {
|
|
19
|
+
"model": self.model,
|
|
20
|
+
"messages": messages,
|
|
21
|
+
"stream": False
|
|
22
|
+
}
|
|
23
|
+
payload.update(kwargs)
|
|
24
|
+
response = requests.post(self.api_url, json=payload)
|
|
25
|
+
response.raise_for_status()
|
|
26
|
+
data = response.json()
|
|
27
|
+
return data.get("message", {}).get("content", data)
|
|
28
|
+
|
|
29
|
+
class LocalProvider(BaseLLMProvider):
|
|
30
|
+
def create_model(self, config: ModelConfig):
|
|
31
|
+
# Extract custom parameters from config model_name & extra_params
|
|
32
|
+
api_url = config.extra_params.get("api_url")
|
|
33
|
+
|
|
34
|
+
return LocalLLM(
|
|
35
|
+
model=config.model_name,
|
|
36
|
+
api_url=api_url
|
|
37
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from langchain_core.language_models import BaseChatModel
|
|
2
|
+
from llm_registry.base import BaseLLMProvider
|
|
3
|
+
from llm_registry.config import ModelConfig
|
|
4
|
+
|
|
5
|
+
class OpenAIProvider(BaseLLMProvider):
|
|
6
|
+
def create_model(self, config: ModelConfig) -> BaseChatModel:
|
|
7
|
+
from langchain_openai import ChatOpenAI
|
|
8
|
+
|
|
9
|
+
kwargs = {
|
|
10
|
+
"model": config.model_name,
|
|
11
|
+
}
|
|
12
|
+
if config.temperature is not None:
|
|
13
|
+
kwargs["temperature"] = config.temperature
|
|
14
|
+
if config.max_tokens is not None:
|
|
15
|
+
kwargs["max_tokens"] = config.max_tokens
|
|
16
|
+
|
|
17
|
+
return ChatOpenAI(
|
|
18
|
+
**kwargs,
|
|
19
|
+
**config.extra_params
|
|
20
|
+
)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
from langchain_core.language_models import BaseChatModel
|
|
3
|
+
|
|
4
|
+
from llm_registry.base import BaseLLMProvider
|
|
5
|
+
from llm_registry.config import ModelConfig
|
|
6
|
+
from llm_registry.errors import (
|
|
7
|
+
LLMErrorDetails,
|
|
8
|
+
LLMInvocationError,
|
|
9
|
+
LLMRegistryError,
|
|
10
|
+
ModelAliasNotFoundError,
|
|
11
|
+
ModelCreationError,
|
|
12
|
+
ProviderDependencyError,
|
|
13
|
+
ProviderNotRegisteredError,
|
|
14
|
+
normalize_exception,
|
|
15
|
+
)
|
|
16
|
+
from llm_registry.providers.openai import OpenAIProvider
|
|
17
|
+
from llm_registry.providers.groq import GroqProvider
|
|
18
|
+
from llm_registry.providers.anthropic import AnthropicProvider
|
|
19
|
+
from llm_registry.providers.google import GoogleProvider
|
|
20
|
+
from llm_registry.providers.local_llm import LocalProvider
|
|
21
|
+
from llm_registry.result import LLMResult
|
|
22
|
+
|
|
23
|
+
class LLMRegistry:
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self._providers: Dict[str, BaseLLMProvider] = {}
|
|
26
|
+
self._configs: Dict[str, ModelConfig] = {}
|
|
27
|
+
|
|
28
|
+
############### Register default providers #######################
|
|
29
|
+
self.register_provider("openai", OpenAIProvider())
|
|
30
|
+
self.register_provider("groq", GroqProvider())
|
|
31
|
+
self.register_provider("anthropic", AnthropicProvider())
|
|
32
|
+
self.register_provider("google", GoogleProvider())
|
|
33
|
+
self.register_provider("local", LocalProvider())
|
|
34
|
+
|
|
35
|
+
def register_provider(self, name: str, provider: BaseLLMProvider):
|
|
36
|
+
"""Register an LLM provider."""
|
|
37
|
+
self._providers[name.lower()] = provider
|
|
38
|
+
|
|
39
|
+
def register_model_config(self, alias: str, config: ModelConfig):
|
|
40
|
+
"""Register a model configuration under an alias."""
|
|
41
|
+
self._configs[alias] = config
|
|
42
|
+
|
|
43
|
+
def get_model(self, alias: str) -> BaseChatModel:
|
|
44
|
+
"""Instantiate and return the model configured for the given alias."""
|
|
45
|
+
if alias not in self._configs:
|
|
46
|
+
details = LLMErrorDetails(
|
|
47
|
+
provider=None,
|
|
48
|
+
model_name=None,
|
|
49
|
+
category="model_alias_not_found",
|
|
50
|
+
message=f"Model alias '{alias}' is not registered in the LLM Registry.",
|
|
51
|
+
exception_type="ModelAliasNotFoundError",
|
|
52
|
+
)
|
|
53
|
+
raise ModelAliasNotFoundError(
|
|
54
|
+
details.message,
|
|
55
|
+
details,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
config = self._configs[alias]
|
|
59
|
+
provider = self._providers.get(config.provider.lower())
|
|
60
|
+
if not provider:
|
|
61
|
+
details = LLMErrorDetails(
|
|
62
|
+
provider=config.provider,
|
|
63
|
+
model_name=config.model_name,
|
|
64
|
+
category="provider_not_registered",
|
|
65
|
+
message=f"No provider implementation found for '{config.provider}'.",
|
|
66
|
+
exception_type="ProviderNotRegisteredError",
|
|
67
|
+
)
|
|
68
|
+
raise ProviderNotRegisteredError(
|
|
69
|
+
details.message,
|
|
70
|
+
details,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
return provider.create_model(config)
|
|
75
|
+
except ImportError as exc:
|
|
76
|
+
details = normalize_exception(
|
|
77
|
+
exc,
|
|
78
|
+
provider=config.provider,
|
|
79
|
+
model_name=config.model_name,
|
|
80
|
+
)
|
|
81
|
+
raise ProviderDependencyError(
|
|
82
|
+
f"Missing dependency for provider '{config.provider}': {exc}",
|
|
83
|
+
details,
|
|
84
|
+
) from exc
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
details = normalize_exception(
|
|
87
|
+
exc,
|
|
88
|
+
provider=config.provider,
|
|
89
|
+
model_name=config.model_name,
|
|
90
|
+
)
|
|
91
|
+
raise ModelCreationError(
|
|
92
|
+
f"Failed to create model '{config.model_name}' for provider '{config.provider}': {exc}",
|
|
93
|
+
details,
|
|
94
|
+
) from exc
|
|
95
|
+
|
|
96
|
+
def invoke(self, alias: str, input: Any, **kwargs: Any) -> Any:
|
|
97
|
+
"""Invoke a registered model and raise normalized package exceptions on failure."""
|
|
98
|
+
config = self._get_config(alias)
|
|
99
|
+
try:
|
|
100
|
+
model = self.get_model(alias)
|
|
101
|
+
return model.invoke(input, **kwargs)
|
|
102
|
+
except LLMRegistryError:
|
|
103
|
+
raise
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
details = normalize_exception(
|
|
106
|
+
exc,
|
|
107
|
+
provider=config.provider,
|
|
108
|
+
model_name=config.model_name,
|
|
109
|
+
)
|
|
110
|
+
raise LLMInvocationError(
|
|
111
|
+
f"LLM API call failed for provider '{config.provider}' and model '{config.model_name}': {exc}",
|
|
112
|
+
details,
|
|
113
|
+
) from exc
|
|
114
|
+
|
|
115
|
+
def safe_invoke(self, alias: str, input: Any, **kwargs: Any) -> LLMResult:
|
|
116
|
+
"""Invoke a registered model and return structured success/error data."""
|
|
117
|
+
try:
|
|
118
|
+
response = self.invoke(alias, input, **kwargs)
|
|
119
|
+
return LLMResult(
|
|
120
|
+
ok=True,
|
|
121
|
+
content=getattr(response, "content", None),
|
|
122
|
+
raw_response=response,
|
|
123
|
+
)
|
|
124
|
+
except LLMRegistryError as exc:
|
|
125
|
+
return LLMResult(ok=False, error=exc.details)
|
|
126
|
+
|
|
127
|
+
def _get_config(self, alias: str) -> ModelConfig:
|
|
128
|
+
if alias not in self._configs:
|
|
129
|
+
details = LLMErrorDetails(
|
|
130
|
+
provider=None,
|
|
131
|
+
model_name=None,
|
|
132
|
+
category="model_alias_not_found",
|
|
133
|
+
message=f"Model alias '{alias}' is not registered in the LLM Registry.",
|
|
134
|
+
exception_type="ModelAliasNotFoundError",
|
|
135
|
+
)
|
|
136
|
+
raise ModelAliasNotFoundError(
|
|
137
|
+
details.message,
|
|
138
|
+
details,
|
|
139
|
+
)
|
|
140
|
+
return self._configs[alias]
|
|
141
|
+
|
|
142
|
+
# Global registry singleton instance
|
|
143
|
+
global_registry = LLMRegistry()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from llm_registry.errors import LLMErrorDetails
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class LLMResult:
|
|
9
|
+
ok: bool
|
|
10
|
+
content: Optional[str] = None
|
|
11
|
+
raw_response: Optional[Any] = None
|
|
12
|
+
error: Optional[LLMErrorDetails] = None
|
|
13
|
+
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
# --- Dynamic Path Configuration ---
|
|
6
|
+
# Detects whether the script is run from /tests or the project root and appends /src
|
|
7
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
8
|
+
if os.path.basename(current_dir) == "tests":
|
|
9
|
+
project_root = os.path.dirname(current_dir)
|
|
10
|
+
else:
|
|
11
|
+
project_root = current_dir
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(project_root, "src"))
|
|
14
|
+
|
|
15
|
+
# Import from your llm_registry package
|
|
16
|
+
from llm_registry import global_registry, ModelConfig
|
|
17
|
+
|
|
18
|
+
# Load API keys from your local .env file
|
|
19
|
+
load_dotenv()
|
|
20
|
+
|
|
21
|
+
# Dictionary to map user choices to model config registrations
|
|
22
|
+
MODELS_MAP = {
|
|
23
|
+
"1": {
|
|
24
|
+
"alias": "openai-chat",
|
|
25
|
+
"name": "OpenAI (gpt-4o-mini)",
|
|
26
|
+
"config": ModelConfig(provider="openai", model_name="gpt-4o-mini", temperature=0.7),
|
|
27
|
+
"pip": "pip install langchain-openai"
|
|
28
|
+
},
|
|
29
|
+
"2": {
|
|
30
|
+
"alias": "groq-chat",
|
|
31
|
+
"name": "Groq (llama-3.1-8b-instant)",
|
|
32
|
+
"config": ModelConfig(provider="groq", model_name="llama-3.1-8b-instant", temperature=0.7),
|
|
33
|
+
"pip": "pip install langchain-groq"
|
|
34
|
+
},
|
|
35
|
+
"3": {
|
|
36
|
+
"alias": "anthropic-chat",
|
|
37
|
+
"name": "Anthropic (claude-opus-4-8)",
|
|
38
|
+
"config": ModelConfig(provider="anthropic", model_name="claude-opus-4-8"),
|
|
39
|
+
"pip": "pip install langchain-anthropic"
|
|
40
|
+
},
|
|
41
|
+
"4": {
|
|
42
|
+
"alias": "gemini-chat",
|
|
43
|
+
"name": "Google Gemini (gemini-2.5-flash)",
|
|
44
|
+
"config": ModelConfig(provider="google", model_name="gemini-2.5-flash", temperature=0.7),
|
|
45
|
+
"pip": "pip install langchain-google-genai"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def register_all_models():
|
|
50
|
+
"""Register all configurations in the global registry."""
|
|
51
|
+
for key, model_info in MODELS_MAP.items():
|
|
52
|
+
global_registry.register_model_config(model_info["alias"], model_info["config"])
|
|
53
|
+
|
|
54
|
+
def run_playground():
|
|
55
|
+
register_all_models()
|
|
56
|
+
|
|
57
|
+
print("=" * 50)
|
|
58
|
+
print(" LLM REGISTRY PLAYGROUND")
|
|
59
|
+
print("=" * 50)
|
|
60
|
+
print("Select a provider/model to chat with:")
|
|
61
|
+
|
|
62
|
+
for key, model_info in MODELS_MAP.items():
|
|
63
|
+
print(f"{key}. {model_info['name']}")
|
|
64
|
+
|
|
65
|
+
choice = input("\nEnter choice (1-4): ").strip()
|
|
66
|
+
|
|
67
|
+
if choice not in MODELS_MAP:
|
|
68
|
+
print("Invalid selection. Exiting.")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
model_info = MODELS_MAP[choice]
|
|
72
|
+
alias = model_info["alias"]
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Resolve model from the registry
|
|
76
|
+
model = global_registry.get_model(alias)
|
|
77
|
+
print(f"\n[Success] Resolved model for: {model_info['name']}")
|
|
78
|
+
print("Type 'exit' or 'quit' to end session.\n")
|
|
79
|
+
|
|
80
|
+
while True:
|
|
81
|
+
user_input = input("You: ").strip()
|
|
82
|
+
if user_input.lower() in ["exit", "quit"]:
|
|
83
|
+
print("Exiting playground...")
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
if not user_input:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
print(f"[{alias} thinking...]")
|
|
90
|
+
result = global_registry.safe_invoke(alias, user_input)
|
|
91
|
+
if result.ok:
|
|
92
|
+
print(f"\nAI: {result.content}\n")
|
|
93
|
+
else:
|
|
94
|
+
error = result.error
|
|
95
|
+
print("\n[API Error]")
|
|
96
|
+
if error is None:
|
|
97
|
+
print("Message: Unknown registry error\n")
|
|
98
|
+
continue
|
|
99
|
+
print(f"Provider: {error.provider}")
|
|
100
|
+
print(f"Model: {error.model_name}")
|
|
101
|
+
print(f"Category: {error.category}")
|
|
102
|
+
if error.status_code is not None:
|
|
103
|
+
print(f"Status Code: {error.status_code}")
|
|
104
|
+
if error.code is not None:
|
|
105
|
+
print(f"Code: {error.code}")
|
|
106
|
+
print(f"Message: {error.message}\n")
|
|
107
|
+
|
|
108
|
+
except ImportError as imp_err:
|
|
109
|
+
print(f"\n[Import Error]: Missing SDK dependencies.")
|
|
110
|
+
print(f"To use this provider, please run: {model_info['pip']}")
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"\n[Error]: {e}")
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
run_playground()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from llm_registry import global_registry, ModelConfig
|
|
2
|
+
|
|
3
|
+
# Register the local model configuration
|
|
4
|
+
global_registry.register_model_config(
|
|
5
|
+
"ollama-chat",
|
|
6
|
+
ModelConfig(
|
|
7
|
+
provider="local",
|
|
8
|
+
model_name="llama3",
|
|
9
|
+
extra_params={"api_url": "http://localhost:11434/api/chat"}
|
|
10
|
+
)
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Resolve and invoke
|
|
14
|
+
local_model = global_registry.get_model("ollama-chat")
|
|
15
|
+
response = local_model.invoke("Hello, local model!")
|
|
16
|
+
print(response) # Outputs the direct text response
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
# --- Dynamic Path Configuration ---
|
|
7
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
8
|
+
project_root = os.path.dirname(current_dir)
|
|
9
|
+
sys.path.insert(0, os.path.join(project_root, "src"))
|
|
10
|
+
|
|
11
|
+
from llm_registry import global_registry, ModelConfig
|
|
12
|
+
from langchain_core.language_models import BaseChatModel
|
|
13
|
+
|
|
14
|
+
# Load API keys from .env file
|
|
15
|
+
load_dotenv()
|
|
16
|
+
|
|
17
|
+
class TestGroqRegistry(unittest.TestCase):
|
|
18
|
+
|
|
19
|
+
def test_env_loaded(self):
|
|
20
|
+
"""Test that the GROQ_API_KEY is successfully loaded from .env"""
|
|
21
|
+
api_key = os.getenv("GROQ_API_KEY")
|
|
22
|
+
self.assertIsNotNone(api_key, "GROQ_API_KEY is not defined in your .env file.")
|
|
23
|
+
|
|
24
|
+
def test_groq_registration_and_retrieval(self):
|
|
25
|
+
"""Test that Groq is registered and resolves successfully from global_registry"""
|
|
26
|
+
config = ModelConfig(
|
|
27
|
+
provider="groq",
|
|
28
|
+
model_name="llama-3.1-8b-instant",
|
|
29
|
+
temperature=0.1
|
|
30
|
+
)
|
|
31
|
+
global_registry.register_model_config("my-groq-model", config)
|
|
32
|
+
|
|
33
|
+
model = global_registry.get_model("my-groq-model")
|
|
34
|
+
|
|
35
|
+
self.assertIsInstance(model, BaseChatModel, "The returned object is not a LangChain model.")
|
|
36
|
+
self.assertEqual(model.model_name, "llama-3.1-8b-instant")
|
|
37
|
+
|
|
38
|
+
def test_live_groq_api_call(self):
|
|
39
|
+
"""Test a live request to Groq using your key"""
|
|
40
|
+
config = ModelConfig(
|
|
41
|
+
provider="groq",
|
|
42
|
+
model_name="llama-3.1-8b-instant",
|
|
43
|
+
temperature=0.0,
|
|
44
|
+
max_tokens=15
|
|
45
|
+
)
|
|
46
|
+
global_registry.register_model_config("groq-test-call", config)
|
|
47
|
+
model = global_registry.get_model("groq-test-call")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
response = model.invoke("Say the word 'success' and nothing else.")
|
|
51
|
+
response_text = response.content.strip().lower()
|
|
52
|
+
|
|
53
|
+
self.assertIn("success", response_text)
|
|
54
|
+
print(f"\n[Success] Live Groq API Connection verified. Model output: {response.content}")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
self.fail(f"Live Groq API call failed with exception: {e}")
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
unittest.main()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
# --- Dynamic Path Configuration ---
|
|
6
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
7
|
+
project_root = os.path.dirname(current_dir)
|
|
8
|
+
sys.path.insert(0, os.path.join(project_root, "src"))
|
|
9
|
+
|
|
10
|
+
from llm_registry import global_registry, ModelConfig
|
|
11
|
+
|
|
12
|
+
# 1. Load your API keys and local configs from .env
|
|
13
|
+
load_dotenv()
|
|
14
|
+
|
|
15
|
+
# Setup simulated env parameters if not already present in the environment/dotenv file
|
|
16
|
+
if not os.getenv("LOCAL_MODEL_LINK"):
|
|
17
|
+
os.environ["LOCAL_MODEL_LINK"] = "http://192.168.101.88:11434/api/chat"
|
|
18
|
+
if not os.getenv("LOCAL_MODEL"):
|
|
19
|
+
os.environ["LOCAL_MODEL"] = "llama3.1:8b"
|
|
20
|
+
|
|
21
|
+
def run_local_model_test():
|
|
22
|
+
print("=" * 50)
|
|
23
|
+
print(" LOCAL OLLAMA CONNECTION TEST")
|
|
24
|
+
print("=" * 50)
|
|
25
|
+
|
|
26
|
+
print(f"Env LOCAL_MODEL_LINK: {os.getenv('LOCAL_MODEL_LINK')}")
|
|
27
|
+
print(f"Env LOCAL_MODEL: {os.getenv('LOCAL_MODEL')}")
|
|
28
|
+
|
|
29
|
+
# Register the local provider config.
|
|
30
|
+
# By passing empty strings/None, it falls back dynamically to your .env variables!
|
|
31
|
+
global_registry.register_model_config(
|
|
32
|
+
"local-ollama",
|
|
33
|
+
ModelConfig(
|
|
34
|
+
provider="local",
|
|
35
|
+
model_name="", # Falls back to os.getenv("LOCAL_MODEL")
|
|
36
|
+
extra_params={} # Falls back to os.getenv("LOCAL_MODEL_LINK")
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
# Resolve the model
|
|
42
|
+
model = global_registry.get_model("local-ollama")
|
|
43
|
+
print("\n[Success] Local model resolved successfully!")
|
|
44
|
+
|
|
45
|
+
# Invoke a simple prompt (role/content layout for Ollama api/chat)
|
|
46
|
+
test_messages = [{"role": "user", "content": "Tell me what is UI"}]
|
|
47
|
+
print(f"Sending request to {model.api_url}...")
|
|
48
|
+
|
|
49
|
+
response = model.invoke(test_messages)
|
|
50
|
+
print(f"\nResponse from Local LLM:\n{response}")
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(f"\n[Connection Error]: {e}")
|
|
54
|
+
print("Please check that your Ollama server is running and accessible at the specified IP/port.")
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
run_local_model_test()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
# --- Dynamic Path Configuration ---
|
|
7
|
+
# This adds the 'src/' folder to Python's search path so it finds 'llm_registry'
|
|
8
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
9
|
+
project_root = os.path.dirname(current_dir)
|
|
10
|
+
sys.path.insert(0, os.path.join(project_root, "src"))
|
|
11
|
+
|
|
12
|
+
# Now these imports will work successfully!
|
|
13
|
+
from llm_registry.providers.openai import OpenAIProvider
|
|
14
|
+
from llm_registry.config import ModelConfig
|
|
15
|
+
from langchain_core.language_models import BaseChatModel
|
|
16
|
+
|
|
17
|
+
# Load API keys from your local .env file
|
|
18
|
+
load_dotenv()
|
|
19
|
+
|
|
20
|
+
class TestOpenAIProvider(unittest.TestCase):
|
|
21
|
+
|
|
22
|
+
def test_env_loaded(self):
|
|
23
|
+
"""Test that the OpenAI API key is successfully loaded from .env"""
|
|
24
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
|
25
|
+
self.assertIsNotNone(api_key, "OPENAI_API_KEY is not defined in your .env file.")
|
|
26
|
+
|
|
27
|
+
def test_provider_initialization(self):
|
|
28
|
+
"""Test that OpenAIProvider instantiates a valid LangChain ChatModel"""
|
|
29
|
+
config = ModelConfig(
|
|
30
|
+
provider="openai",
|
|
31
|
+
model_name="gpt-4o-mini",
|
|
32
|
+
temperature=0.0
|
|
33
|
+
)
|
|
34
|
+
provider = OpenAIProvider()
|
|
35
|
+
model = provider.create_model(config)
|
|
36
|
+
|
|
37
|
+
self.assertIsInstance(model, BaseChatModel, "The returned object is not a LangChain model.")
|
|
38
|
+
self.assertEqual(model.model_name, "gpt-4o-mini")
|
|
39
|
+
|
|
40
|
+
def test_live_api_call(self):
|
|
41
|
+
"""Test a live request to OpenAI using your key"""
|
|
42
|
+
config = ModelConfig(
|
|
43
|
+
provider="openai",
|
|
44
|
+
model_name="gpt-4.1-mini",
|
|
45
|
+
temperature=0.0,
|
|
46
|
+
max_tokens=200
|
|
47
|
+
)
|
|
48
|
+
provider = OpenAIProvider()
|
|
49
|
+
model = provider.create_model(config)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
response = model.invoke("Say the word 'success' and nothing else.")
|
|
53
|
+
response_text = response.content.strip().lower()
|
|
54
|
+
|
|
55
|
+
self.assertIn("success", response_text)
|
|
56
|
+
print(f"\n[Success] Live API Connection verified. Model output: {response.content}")
|
|
57
|
+
except Exception as e:
|
|
58
|
+
self.fail(f"Live API call failed with exception: {e}")
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
unittest.main()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
# --- Dynamic Path Configuration ---
|
|
6
|
+
# Detects whether the script is run from /tests or the project root and appends /src
|
|
7
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
8
|
+
if os.path.basename(current_dir) == "tests":
|
|
9
|
+
project_root = os.path.dirname(current_dir)
|
|
10
|
+
else:
|
|
11
|
+
project_root = current_dir
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(project_root, "src"))
|
|
14
|
+
|
|
15
|
+
from llm_registry import ( # noqa: E402
|
|
16
|
+
BaseLLMProvider,
|
|
17
|
+
LLMInvocationError,
|
|
18
|
+
LLMRegistry,
|
|
19
|
+
ModelAliasNotFoundError,
|
|
20
|
+
ModelConfig,
|
|
21
|
+
ProviderDependencyError,
|
|
22
|
+
ProviderNotRegisteredError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FakeResponse:
|
|
27
|
+
content = "success"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FakeModel:
|
|
31
|
+
def __init__(self, exc=None):
|
|
32
|
+
self.exc = exc
|
|
33
|
+
|
|
34
|
+
def invoke(self, input, **kwargs):
|
|
35
|
+
if self.exc:
|
|
36
|
+
raise self.exc
|
|
37
|
+
return FakeResponse()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FakeProvider(BaseLLMProvider):
|
|
41
|
+
def __init__(self, model=None, exc=None):
|
|
42
|
+
self.model = model or FakeModel()
|
|
43
|
+
self.exc = exc
|
|
44
|
+
|
|
45
|
+
def create_model(self, config):
|
|
46
|
+
if self.exc:
|
|
47
|
+
raise self.exc
|
|
48
|
+
return self.model
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FakeRateLimitError(Exception):
|
|
52
|
+
status_code = 429
|
|
53
|
+
code = "rate_limit_exceeded"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestRegistryErrorHandling(unittest.TestCase):
|
|
57
|
+
def test_unknown_alias_raises_typed_error(self):
|
|
58
|
+
registry = LLMRegistry()
|
|
59
|
+
|
|
60
|
+
with self.assertRaises(ModelAliasNotFoundError) as ctx:
|
|
61
|
+
registry.get_model("missing")
|
|
62
|
+
|
|
63
|
+
self.assertEqual(ctx.exception.details.category, "model_alias_not_found")
|
|
64
|
+
|
|
65
|
+
def test_unknown_provider_raises_typed_error(self):
|
|
66
|
+
registry = LLMRegistry()
|
|
67
|
+
registry.register_model_config(
|
|
68
|
+
"bad-provider",
|
|
69
|
+
ModelConfig(provider="missing", model_name="x"),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
with self.assertRaises(ProviderNotRegisteredError) as ctx:
|
|
73
|
+
registry.get_model("bad-provider")
|
|
74
|
+
|
|
75
|
+
self.assertEqual(ctx.exception.details.provider, "missing")
|
|
76
|
+
self.assertEqual(ctx.exception.details.category, "provider_not_registered")
|
|
77
|
+
|
|
78
|
+
def test_missing_dependency_is_normalized(self):
|
|
79
|
+
registry = LLMRegistry()
|
|
80
|
+
registry.register_provider("fake", FakeProvider(exc=ImportError("fake-sdk")))
|
|
81
|
+
registry.register_model_config(
|
|
82
|
+
"fake",
|
|
83
|
+
ModelConfig(provider="fake", model_name="fake-model"),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
with self.assertRaises(ProviderDependencyError) as ctx:
|
|
87
|
+
registry.get_model("fake")
|
|
88
|
+
|
|
89
|
+
self.assertEqual(ctx.exception.details.provider, "fake")
|
|
90
|
+
self.assertEqual(ctx.exception.details.model_name, "fake-model")
|
|
91
|
+
|
|
92
|
+
def test_invoke_wraps_provider_api_error(self):
|
|
93
|
+
registry = LLMRegistry()
|
|
94
|
+
registry.register_provider("fake", FakeProvider(model=FakeModel(FakeRateLimitError("slow down"))))
|
|
95
|
+
registry.register_model_config(
|
|
96
|
+
"fake",
|
|
97
|
+
ModelConfig(provider="fake", model_name="fake-model"),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
with self.assertRaises(LLMInvocationError) as ctx:
|
|
101
|
+
registry.invoke("fake", "hello")
|
|
102
|
+
|
|
103
|
+
self.assertEqual(ctx.exception.details.category, "rate_limit")
|
|
104
|
+
self.assertEqual(ctx.exception.details.status_code, 429)
|
|
105
|
+
|
|
106
|
+
def test_safe_invoke_returns_structured_result(self):
|
|
107
|
+
registry = LLMRegistry()
|
|
108
|
+
registry.register_provider("fake", FakeProvider(model=FakeModel(FakeRateLimitError("slow down"))))
|
|
109
|
+
registry.register_model_config(
|
|
110
|
+
"fake",
|
|
111
|
+
ModelConfig(provider="fake", model_name="fake-model"),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
result = registry.safe_invoke("fake", "hello")
|
|
115
|
+
|
|
116
|
+
self.assertFalse(result.ok)
|
|
117
|
+
self.assertEqual(result.error.category, "rate_limit")
|
|
118
|
+
self.assertIsNone(result.content)
|
|
119
|
+
|
|
120
|
+
def test_safe_invoke_returns_success_result(self):
|
|
121
|
+
registry = LLMRegistry()
|
|
122
|
+
registry.register_provider("fake", FakeProvider())
|
|
123
|
+
registry.register_model_config(
|
|
124
|
+
"fake",
|
|
125
|
+
ModelConfig(provider="fake", model_name="fake-model"),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
result = registry.safe_invoke("fake", "hello")
|
|
129
|
+
|
|
130
|
+
self.assertTrue(result.ok)
|
|
131
|
+
self.assertEqual(result.content, "success")
|
|
132
|
+
self.assertIsInstance(result.raw_response, FakeResponse)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
unittest.main()
|