lmcore 0.1.1__py3-none-any.whl

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.
lmcore/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from .config import load_config
2
+ from .llm import get_chat_client, get_embed_client, register_provider
3
+ from .logging_utils import configure_logging, get_logger, Colors
4
+
5
+ __all__ = [
6
+ "load_config",
7
+ "get_chat_client",
8
+ "get_embed_client",
9
+ "register_provider",
10
+ "configure_logging",
11
+ "get_logger",
12
+ "Colors",
13
+ ]
lmcore/config.py ADDED
@@ -0,0 +1,24 @@
1
+ from dotenv import load_dotenv
2
+ import os
3
+
4
+
5
+ def load_config(required: list[str], optional: dict[str, str] = {}) -> dict:
6
+ load_dotenv()
7
+
8
+ config = {}
9
+ missing = []
10
+
11
+ for key in required:
12
+ val = os.getenv(key)
13
+ if val is None:
14
+ missing.append(key)
15
+ else:
16
+ config[key] = val
17
+
18
+ if missing:
19
+ raise EnvironmentError(f"Missing required env vars: {missing}")
20
+
21
+ for key, default in optional.items():
22
+ config[key] = os.getenv(key, default)
23
+
24
+ return config
lmcore/llm.py ADDED
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+ import yaml
3
+ import os
4
+ from typing import Callable
5
+
6
+ _registry: dict[str, Callable] = {}
7
+
8
+
9
+ def register_provider(name: str, factory_fn: Callable) -> None:
10
+ _registry[name] = factory_fn
11
+
12
+
13
+ def _load_arch(models_yaml_path: str, arch: str) -> dict:
14
+ if not os.path.exists(models_yaml_path):
15
+ raise FileNotFoundError(f"models.yaml not found at: {models_yaml_path}")
16
+
17
+ with open(models_yaml_path, "r") as f:
18
+ data = yaml.safe_load(f)
19
+
20
+ if arch not in data:
21
+ available = list(data.keys())
22
+ raise KeyError(f"Arch '{arch}' not found in {models_yaml_path}. Available: {available}")
23
+
24
+ return data[arch]
25
+
26
+
27
+ def _resolve_api_key(cfg: dict) -> dict:
28
+ if "api_key_env" in cfg:
29
+ key = os.getenv(cfg["api_key_env"])
30
+ if key is None:
31
+ raise EnvironmentError(f"Env var '{cfg['api_key_env']}' is not set")
32
+ cfg = {**cfg, "api_key": key}
33
+ if "base_url_env" in cfg:
34
+ url = os.getenv(cfg["base_url_env"])
35
+ if url is None:
36
+ raise EnvironmentError(f"Env var '{cfg['base_url_env']}' is not set")
37
+ cfg = {**cfg, "base_url": url}
38
+ return cfg
39
+
40
+
41
+ def get_chat_client(models_yaml_path: str, arch: str, index: int = 0):
42
+ arch_config = _load_arch(models_yaml_path, arch)
43
+
44
+ if "chat" not in arch_config:
45
+ raise ValueError(f"Arch '{arch}' has no chat config in {models_yaml_path}")
46
+
47
+ clients = arch_config["chat"]
48
+ if index >= len(clients):
49
+ raise IndexError(
50
+ f"Arch '{arch}' has {len(clients)} chat client(s), index {index} is out of range"
51
+ )
52
+
53
+ cfg = _resolve_api_key(clients[index])
54
+ provider = cfg["provider"]
55
+
56
+ if provider not in _registry:
57
+ raise ValueError(f"Provider '{provider}' is not registered. Use register_provider() to add it.")
58
+
59
+ return _registry[provider](cfg)
60
+
61
+
62
+ def get_embed_client(models_yaml_path: str, arch: str, index: int = 0):
63
+ arch_config = _load_arch(models_yaml_path, arch)
64
+
65
+ if "embed" not in arch_config:
66
+ raise ValueError(f"Arch '{arch}' has no embed config in {models_yaml_path}")
67
+
68
+ clients = arch_config["embed"]
69
+ if index >= len(clients):
70
+ raise IndexError(
71
+ f"Arch '{arch}' has {len(clients)} embed client(s), index {index} is out of range"
72
+ )
73
+
74
+ cfg = _resolve_api_key(clients[index])
75
+ provider = cfg["provider"]
76
+
77
+ if provider not in _registry:
78
+ raise ValueError(f"Provider '{provider}' is not registered. Use register_provider() to add it.")
79
+
80
+ return _registry[provider](cfg)
81
+
82
+
83
+ # --- Built-in provider factories ---
84
+
85
+ def _build_ollama(cfg: dict):
86
+ from ollama import Client
87
+ return Client(host=cfg.get("base_url", "http://localhost:11434"))
88
+
89
+
90
+ def _build_openai(cfg: dict):
91
+ from openai import OpenAI
92
+ return OpenAI(api_key=cfg["api_key"], base_url=cfg.get("base_url"))
93
+
94
+
95
+ def _build_anthropic(cfg: dict):
96
+ from anthropic import Anthropic
97
+ return Anthropic(api_key=cfg["api_key"])
98
+
99
+
100
+ register_provider("ollama", _build_ollama)
101
+ register_provider("openai", _build_openai)
102
+ register_provider("anthropic", _build_anthropic)
@@ -0,0 +1,49 @@
1
+ import logging
2
+
3
+
4
+ class Colors:
5
+ HEADER = '\033[95m'
6
+ OKBLUE = '\033[94m'
7
+ OKCYAN = '\033[96m'
8
+ OKGREEN = '\033[92m'
9
+ WARNING = '\033[93m'
10
+ FAIL = '\033[91m'
11
+ ENDC = '\033[0m'
12
+ BOLD = '\033[1m'
13
+ UNDERLINE = '\033[4m'
14
+ DIM = '\033[2m'
15
+
16
+
17
+ class _ColorFormatter(logging.Formatter):
18
+ _LEVEL_COLORS = {
19
+ logging.DEBUG: Colors.DIM,
20
+ logging.INFO: Colors.OKCYAN,
21
+ logging.WARNING: Colors.WARNING,
22
+ logging.ERROR: Colors.FAIL,
23
+ logging.CRITICAL: Colors.FAIL + Colors.BOLD,
24
+ }
25
+
26
+ def format(self, record: logging.LogRecord) -> str:
27
+ color = self._LEVEL_COLORS.get(record.levelno, "")
28
+ record.levelname = f"{color}{record.levelname}{Colors.ENDC}"
29
+ record.name = f"{Colors.OKBLUE}{record.name}{Colors.ENDC}"
30
+ return super().format(record)
31
+
32
+
33
+ def configure_logging(level: str = "INFO") -> None:
34
+ numeric = getattr(logging, level.upper(), logging.INFO)
35
+
36
+ handler = logging.StreamHandler()
37
+ handler.setFormatter(
38
+ _ColorFormatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
39
+ )
40
+
41
+ root = logging.getLogger()
42
+ if not root.handlers:
43
+ root.addHandler(handler)
44
+
45
+ root.setLevel(numeric)
46
+
47
+
48
+ def get_logger(name: str) -> logging.Logger:
49
+ return logging.getLogger(name)
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.4
2
+ Name: lmcore
3
+ Version: 0.1.1
4
+ Summary: Personal core toolkit for AI projects — config loading, LLM client factory, and logging.
5
+ Author-email: Luigi Medrano <luigimedrano03@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/lm45562/lmcore
8
+ Project-URL: Repository, https://github.com/lm45562/lmcore
9
+ Keywords: ai,llm,openai,anthropic,ollama,config,logging
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: python-dotenv>=1.0.0
16
+ Requires-Dist: pyyaml>=6.0
17
+ Provides-Extra: ollama
18
+ Requires-Dist: ollama>=0.2.0; extra == "ollama"
19
+ Provides-Extra: openai
20
+ Requires-Dist: openai>=1.0.0; extra == "openai"
21
+ Provides-Extra: anthropic
22
+ Requires-Dist: anthropic>=0.25.0; extra == "anthropic"
23
+ Provides-Extra: all
24
+ Requires-Dist: ollama>=0.2.0; extra == "all"
25
+ Requires-Dist: openai>=1.0.0; extra == "all"
26
+ Requires-Dist: anthropic>=0.25.0; extra == "all"
27
+
28
+ # lmcore
29
+
30
+ Personal core toolkit for AI projects. Install once, call with inputs, never edit the package.
31
+
32
+ Covers three things every project needs:
33
+ - **Config** — load and validate `.env` secrets
34
+ - **LLM** — build AI clients from a `models.yaml` file
35
+ - **Logging** — colored, consistent logging across all modules
36
+
37
+ ---
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install lmcore
43
+ ```
44
+
45
+ With specific provider dependencies:
46
+
47
+ ```bash
48
+ pip install lmcore[openai]
49
+ pip install lmcore[anthropic]
50
+ pip install lmcore[ollama]
51
+ pip install lmcore[all] # all three providers
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Config
57
+
58
+ Call `load_config` once at startup. Pass the keys your project needs — the package handles loading from `.env`, validating, and returning a plain dict.
59
+
60
+ ```python
61
+ from lmcore import load_config
62
+
63
+ cfg = load_config(
64
+ required=["OPENAI_API_KEY", "CHROMA_API_KEY"],
65
+ optional={"ACTIVE_ARCH": "arch_1", "LOG_LEVEL": "INFO"}
66
+ )
67
+ ```
68
+
69
+ - `required` — must exist in `.env`, raises `EnvironmentError` if any are missing
70
+ - `optional` — uses the default value if not set in `.env`
71
+
72
+ ---
73
+
74
+ ## LLM
75
+
76
+ ### models.yaml
77
+
78
+ Lives in your **project root**. Define only what your project uses — `chat` and `embed` are both optional. Each section is a list so you can define multiple clients and reference them by index.
79
+
80
+ ```yaml
81
+ arch_1:
82
+ chat:
83
+ - provider: openai
84
+ model: gpt-4o
85
+ api_key_env: OPENAI_API_KEY
86
+ - provider: anthropic
87
+ model: claude-sonnet-4-6
88
+ api_key_env: ANTHROPIC_API_KEY
89
+ embed:
90
+ - provider: ollama
91
+ model: nomic-embed-text
92
+ base_url_env: OLLAMA_URL
93
+
94
+ arch_2:
95
+ chat:
96
+ - provider: anthropic
97
+ model: claude-opus-4-8
98
+ api_key_env: ANTHROPIC_API_KEY
99
+ ```
100
+
101
+ **Supported fields per client:**
102
+
103
+ | Field | Required | Description |
104
+ |---|---|---|
105
+ | `provider` | yes | `ollama`, `openai`, or `anthropic` |
106
+ | `model` | yes | model name string |
107
+ | `api_key_env` | when needed | env var name that holds the API key |
108
+ | `base_url` | no | override default endpoint (e.g. local Ollama) |
109
+ | `base_url_env` | no | env var name that holds the base URL (alternative to `base_url`) |
110
+
111
+ ### Getting clients
112
+
113
+ ```python
114
+ from lmcore import get_chat_client, get_embed_client
115
+
116
+ # index defaults to 0 — covers most single-client projects
117
+ chat = get_chat_client("models.yaml", arch="arch_1")
118
+ embed = get_embed_client("models.yaml", arch="arch_1")
119
+
120
+ # specify index for multiple clients
121
+ openai_client = get_chat_client("models.yaml", arch="arch_1", index=0)
122
+ anthropic_client = get_chat_client("models.yaml", arch="arch_1", index=1)
123
+ ```
124
+
125
+ ### Adding a custom provider
126
+
127
+ If you need a provider not built into the package, register it in your project:
128
+
129
+ ```python
130
+ from lmcore import register_provider
131
+
132
+ def _build_groq(cfg):
133
+ from groq import Groq
134
+ return Groq(api_key=cfg["api_key"])
135
+
136
+ register_provider("groq", _build_groq)
137
+ ```
138
+
139
+ Once registered, `groq` works as a `provider:` value in `models.yaml` like any built-in.
140
+
141
+ ---
142
+
143
+ ## Logging
144
+
145
+ ```python
146
+ from lmcore import configure_logging, get_logger
147
+
148
+ configure_logging() # call once at app startup
149
+ configure_logging("DEBUG") # optional level override
150
+
151
+ logger = get_logger(__name__)
152
+
153
+ logger.info("Starting up")
154
+ logger.warning("Something looks off")
155
+ logger.error("Connection failed")
156
+ ```
157
+
158
+ Color scheme:
159
+
160
+ | Color | Level |
161
+ |---|---|
162
+ | Cyan | INFO |
163
+ | Yellow | WARNING |
164
+ | Red | ERROR / CRITICAL |
165
+ | Dim | DEBUG |
166
+
167
+ ---
168
+
169
+ ## Full Example
170
+
171
+ **Project layout:**
172
+ ```
173
+ my-project/
174
+ ├── .env
175
+ ├── models.yaml
176
+ └── app/
177
+ └── core/
178
+ ├── config.py
179
+ └── llm.py
180
+ ```
181
+
182
+ **`.env`:**
183
+ ```
184
+ OPENAI_API_KEY=sk-...
185
+ ACTIVE_ARCH=arch_1
186
+ ```
187
+
188
+ **`models.yaml`:**
189
+ ```yaml
190
+ arch_1:
191
+ chat:
192
+ - provider: openai
193
+ model: gpt-4o
194
+ api_key_env: OPENAI_API_KEY
195
+ - provider: ollama
196
+ model: llama3
197
+ base_url_env: OLLAMA_URL
198
+ ```
199
+
200
+ **`app/core/config.py`:**
201
+ ```python
202
+ from lmcore import load_config, configure_logging
203
+
204
+ configure_logging()
205
+
206
+ cfg = load_config(
207
+ required=["OPENAI_API_KEY"],
208
+ optional={"ACTIVE_ARCH": "arch_1"}
209
+ )
210
+ ```
211
+
212
+ **`app/core/llm.py`:**
213
+ ```python
214
+ from lmcore import get_chat_client
215
+ from app.core.config import cfg
216
+
217
+ chat = get_chat_client("models.yaml", arch=cfg["ACTIVE_ARCH"])
218
+ ```
219
+
220
+ **Anywhere in the project:**
221
+ ```python
222
+ from lmcore import get_logger
223
+ from app.core.llm import chat
224
+
225
+ logger = get_logger(__name__)
226
+
227
+ response = chat.chat(
228
+ model="gpt-4o",
229
+ messages=[{"role": "user", "content": "Summarize this document."}]
230
+ )
231
+ logger.info(f"Done: {response.choices[0].message.content}")
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Built-in Providers
237
+
238
+ | Provider | `provider:` value | Install extra |
239
+ |---|---|---|
240
+ | Ollama | `ollama` | `pip install lmcore[ollama]` |
241
+ | OpenAI | `openai` | `pip install lmcore[openai]` |
242
+ | Anthropic | `anthropic` | `pip install lmcore[anthropic]` |
@@ -0,0 +1,8 @@
1
+ lmcore/__init__.py,sha256=3V0bvc0IzxKobeEOAet0vbDl8KgOaoQn54n-KFd-P6Q,330
2
+ lmcore/config.py,sha256=HxKjWKUQSMeVBSl5gaN13BfZmfMFz72NS6FxwjK6FlU,524
3
+ lmcore/llm.py,sha256=UWlNs2A7MhHA9f9v7RDO3waAQ21Xe3z18051iBGgP7o,3109
4
+ lmcore/logging_utils.py,sha256=8gDxkZupI_JN9nxTRy1XYHfV-tWJ3BrMqVtXexf8ytE,1338
5
+ lmcore-0.1.1.dist-info/METADATA,sha256=FyvwOYjjobVVoreIcuqkSBgt9f4-XjQjuJlq_A5zdMM,5806
6
+ lmcore-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ lmcore-0.1.1.dist-info/top_level.txt,sha256=tM4MX4grJmBML_00ZV6k23k1SLugbZ42qhRWi3TZjTk,7
8
+ lmcore-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ lmcore