python-infrakit-dev 0.1.4__py3-none-any.whl → 0.1.6__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.
@@ -0,0 +1,167 @@
1
+ """
2
+ infrakit.llm.providers.groq
3
+ -----------------------------
4
+ Groq provider — wraps the groq Python SDK.
5
+
6
+ Install dependency::
7
+
8
+ pip install groq
9
+
10
+ Supported models (default): llama-3.3-70b-versatile
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import time
16
+ from typing import Any, Optional, Type
17
+
18
+ from pydantic import BaseModel
19
+
20
+ from ..models import LLMResponse, Prompt
21
+ from .base import BaseProvider
22
+
23
+
24
+ class GroqProvider(BaseProvider):
25
+ """
26
+ Provider for Groq's Chat Completions API.
27
+
28
+ Parameters
29
+ ----------
30
+ model Model string to use. Defaults to ``llama-3.3-70b-versatile``.
31
+ """
32
+
33
+ DEFAULT_MODEL = "llama-3.3-70b-versatile"
34
+ PROVIDER_NAME = "groq"
35
+
36
+ def __init__(self, model: Optional[str] = None) -> None:
37
+ super().__init__(model)
38
+ self._check_sdk()
39
+
40
+ # ── public interface ───────────────────────────────────────────────────
41
+
42
+ async def async_generate(
43
+ self,
44
+ prompt: Prompt,
45
+ api_key: str,
46
+ response_model: Optional[Type[BaseModel]] = None,
47
+ schema_retries: int = 2,
48
+ **kwargs: Any,
49
+ ) -> LLMResponse:
50
+ """Async generate using groq.AsyncGroq."""
51
+ from groq import AsyncGroq
52
+
53
+ client = AsyncGroq(api_key=api_key)
54
+ messages = self._build_messages(prompt)
55
+ t0 = time.perf_counter()
56
+
57
+ response = await client.chat.completions.create(
58
+ model=self.model,
59
+ messages=messages,
60
+ **kwargs,
61
+ )
62
+
63
+ latency_ms = (time.perf_counter() - t0) * 1000
64
+ return self._build_response(response, latency_ms, response_model, schema_retries, api_key)
65
+
66
+ def sync_generate(
67
+ self,
68
+ prompt: Prompt,
69
+ api_key: str,
70
+ response_model: Optional[Type[BaseModel]] = None,
71
+ schema_retries: int = 2,
72
+ **kwargs: Any,
73
+ ) -> LLMResponse:
74
+ """Sync generate using groq.Groq (the blocking SDK client)."""
75
+ from groq import Groq
76
+
77
+ client = Groq(api_key=api_key)
78
+ messages = self._build_messages(prompt)
79
+ t0 = time.perf_counter()
80
+
81
+ response = client.chat.completions.create(
82
+ model=self.model,
83
+ messages=messages,
84
+ **kwargs,
85
+ )
86
+
87
+ latency_ms = (time.perf_counter() - t0) * 1000
88
+ return self._build_response(response, latency_ms, response_model, schema_retries, api_key)
89
+
90
+ # ── internal helpers ───────────────────────────────────────────────────
91
+
92
+ @staticmethod
93
+ def _build_messages(prompt: Prompt) -> list[dict]:
94
+ messages = []
95
+ if prompt.system:
96
+ messages.append({"role": "system", "content": prompt.system})
97
+ messages.append({"role": "user", "content": prompt.user})
98
+ return messages
99
+
100
+ def _build_response(
101
+ self,
102
+ response: Any,
103
+ latency_ms: float,
104
+ response_model: Optional[Type[BaseModel]],
105
+ schema_retries: int,
106
+ api_key: str,
107
+ ) -> LLMResponse:
108
+ choice = response.choices[0]
109
+ content = choice.message.content or ""
110
+
111
+ usage = response.usage
112
+ input_tokens = usage.prompt_tokens if usage else 0
113
+ output_tokens = usage.completion_tokens if usage else 0
114
+ total_tokens = usage.total_tokens if usage else 0
115
+
116
+ parsed = None
117
+ schema_matched = False
118
+
119
+ if response_model is not None:
120
+ parsed, schema_matched = self._validate_schema(
121
+ content, response_model, schema_retries
122
+ )
123
+
124
+ return LLMResponse(
125
+ content=content,
126
+ parsed=parsed,
127
+ schema_matched=schema_matched,
128
+ provider=self.PROVIDER_NAME,
129
+ model=self.model,
130
+ key_id=api_key[:8],
131
+ input_tokens=input_tokens,
132
+ output_tokens=output_tokens,
133
+ total_tokens=total_tokens,
134
+ latency_ms=latency_ms,
135
+ )
136
+
137
+ @staticmethod
138
+ def _check_sdk() -> None:
139
+ try:
140
+ import groq # noqa: F401
141
+ except ImportError as exc:
142
+ raise ImportError(
143
+ "groq package is required for GroqProvider. "
144
+ "Install it with: pip install groq"
145
+ ) from exc
146
+
147
+ @staticmethod
148
+ def _is_quota_error(exc: Exception) -> bool:
149
+ """
150
+ Groq-specific quota/auth HTTP status codes.
151
+
152
+ 429 with "rate_limit_exceeded" is *transient* (burst limit) and should
153
+ be retried with backoff — NOT treated as a permanent quota exhaustion.
154
+ Only 401/402 and 429 with "quota" or "billing" in the message warrant
155
+ immediate model deactivation.
156
+ """
157
+ try:
158
+ from groq import APIStatusError
159
+ if isinstance(exc, APIStatusError):
160
+ if exc.status_code in (401, 402):
161
+ return True
162
+ if exc.status_code == 429:
163
+ msg = str(exc).lower()
164
+ return "quota" in msg or "billing" in msg
165
+ except ImportError:
166
+ pass
167
+ return BaseProvider._is_quota_error(exc)
infrakit/scaffolder/ai.py CHANGED
@@ -40,6 +40,8 @@ from infrakit.scaffolder.generator import (
40
40
  _write,
41
41
  _config_content,
42
42
  _gitignore,
43
+ _infrakit_dep,
44
+ _pkg_dep,
43
45
  _logger_util,
44
46
  _src_init,
45
47
  _tests_init,
@@ -124,9 +126,11 @@ def _load_keys() -> dict:
124
126
  # fall back to keys declared in the config file
125
127
  openai_key = _cfg.get("OPENAI_API_KEY", "")
126
128
  gemini_key = _cfg.get("GEMINI_API_KEY", "")
129
+ groq_key = _cfg.get("GROQ_API_KEY", "")
127
130
  return {{
128
131
  "openai_keys": [openai_key] if openai_key else [],
129
132
  "gemini_keys": [gemini_key] if gemini_key else [],
133
+ "groq_keys": [groq_key] if groq_key else [],
130
134
  }}
131
135
 
132
136
 
@@ -142,6 +146,7 @@ llm: LLMClient = LLMClient(
142
146
  max_concurrent=int(_cfg.get("LLM_CONCURRENCY", 3)),
143
147
  openai_model=_cfg.get("OPENAI_MODEL") or None,
144
148
  gemini_model=_cfg.get("GEMINI_MODEL") or None,
149
+ groq_model=_cfg.get("GROQ_MODEL") or None,
145
150
  )
146
151
 
147
152
  __all__ = ["llm", "Prompt"]
@@ -316,7 +321,8 @@ def _keys_json_template() -> str:
316
321
  {
317
322
  "_comment": "Fill in your API keys. Never commit this file — it is in .gitignore.",
318
323
  "openai_keys": [],
319
- "gemini_keys": []
324
+ "gemini_keys": [],
325
+ "groq_keys": []
320
326
  }
321
327
  """
322
328
 
@@ -401,6 +407,12 @@ pytest
401
407
 
402
408
  def _ai_pyproject(project_name: str, version: str, description: str, author: str) -> str:
403
409
  author_line = f' "{author}",' if author else ' # "Your Name <you@example.com>",'
410
+ infrakit_dep = _infrakit_dep()
411
+ openai_dep = _pkg_dep("openai")
412
+ genai_dep = _pkg_dep("google-genai")
413
+ groq_dep = _pkg_dep("groq")
414
+ pydantic_dep = _pkg_dep("pydantic")
415
+ tqdm_dep = _pkg_dep("tqdm")
404
416
  return f"""\
405
417
  [project]
406
418
  name = "{project_name}"
@@ -413,11 +425,12 @@ authors = [
413
425
  ]
414
426
 
415
427
  dependencies = [
416
- "infrakit",
417
- "openai",
418
- "google-genai",
419
- "pydantic>=2.0",
420
- "tqdm",
428
+ {infrakit_dep},
429
+ "{openai_dep}",
430
+ "{genai_dep}",
431
+ "{groq_dep}",
432
+ "{pydantic_dep}",
433
+ "{tqdm_dep}",
421
434
  ]
422
435
 
423
436
  [project.optional-dependencies]
@@ -48,6 +48,8 @@ from infrakit.scaffolder.generator import (
48
48
  _mkdir,
49
49
  _write,
50
50
  _gitignore,
51
+ _infrakit_dep,
52
+ _pkg_dep,
51
53
  _logger_util,
52
54
  _src_init,
53
55
  _tests_init,
@@ -64,10 +66,12 @@ def _backend_env_config(project_name: str, include_llm: bool) -> str:
64
66
  LLM_KEYS_FILE=keys.json
65
67
  OPENAI_API_KEY=
66
68
  GEMINI_API_KEY=
69
+ GROQ_API_KEY=
67
70
  LLM_MODE=async
68
71
  LLM_CONCURRENCY=3
69
72
  # OPENAI_MODEL=gpt-4o
70
73
  # GEMINI_MODEL=gemini-2.0-flash
74
+ # GROQ_MODEL=llama-3.3-70b-versatile
71
75
  """ if include_llm else ""
72
76
  return f"""\
73
77
  # Application
@@ -96,10 +100,12 @@ def _backend_yaml_config(project_name: str, include_llm: bool) -> str:
96
100
  LLM_KEYS_FILE: keys.json
97
101
  OPENAI_API_KEY: ""
98
102
  GEMINI_API_KEY: ""
103
+ GROQ_API_KEY: ""
99
104
  LLM_MODE: async
100
105
  LLM_CONCURRENCY: 3
101
106
  # OPENAI_MODEL: gpt-4o
102
107
  # GEMINI_MODEL: gemini-2.0-flash
108
+ # GROQ_MODEL: llama-3.3-70b-versatile
103
109
  """ if include_llm else ""
104
110
  return f"""\
105
111
  # Application configuration
@@ -127,6 +133,7 @@ def _backend_json_config(project_name: str, include_llm: bool) -> str:
127
133
  "LLM_KEYS_FILE": "keys.json",
128
134
  "OPENAI_API_KEY": "",
129
135
  "GEMINI_API_KEY": "",
136
+ "GROQ_API_KEY": "",
130
137
  "LLM_MODE": "async",
131
138
  "LLM_CONCURRENCY": 3""" if include_llm else ""
132
139
  return f"""\
@@ -195,6 +202,7 @@ class Settings(BaseSettings):
195
202
  # optional: LLM keys (override ~/.infrakit/llm/ defaults)
196
203
  openai_api_key: str = ""
197
204
  gemini_api_key: str = ""
205
+ groq_api_key: str = ""
198
206
  llm_mode: str = "async"
199
207
  llm_concurrency: int = 3
200
208
 
@@ -448,7 +456,16 @@ services:
448
456
  def _backend_pyproject(
449
457
  project_name: str, version: str, description: str, author: str
450
458
  ) -> str:
451
- author_line = f' "{author}",' if author else ' # "Your Name <you@example.com>",'
459
+ author_line = f' "{author}",' if author else ' # "Your Name <you@example.com>",'
460
+ infrakit_dep = _infrakit_dep()
461
+ fastapi_dep = _pkg_dep("fastapi")
462
+ uvicorn_dep = _pkg_dep("uvicorn")
463
+ pydantic_dep = _pkg_dep("pydantic")
464
+ pydset_dep = _pkg_dep("pydantic-settings")
465
+ openai_dep = _pkg_dep("openai")
466
+ genai_dep = _pkg_dep("google-genai")
467
+ groq_dep = _pkg_dep("groq")
468
+ tqdm_dep = _pkg_dep("tqdm")
452
469
  return f"""\
453
470
  [project]
454
471
  name = "{project_name}"
@@ -461,14 +478,15 @@ authors = [
461
478
  ]
462
479
 
463
480
  dependencies = [
464
- "infrakit",
465
- "fastapi>=0.110",
466
- "uvicorn[standard]",
467
- "pydantic>=2.0",
468
- "pydantic-settings",
469
- "openai",
470
- "google-generativeai",
471
- "tqdm",
481
+ {infrakit_dep},
482
+ "{fastapi_dep}",
483
+ "{uvicorn_dep}[standard]",
484
+ "{pydantic_dep}",
485
+ "{pydset_dep}",
486
+ "{openai_dep}",
487
+ "{genai_dep}",
488
+ "{groq_dep}",
489
+ "{tqdm_dep}",
472
490
  ]
473
491
 
474
492
  [project.optional-dependencies]
@@ -40,6 +40,8 @@ from infrakit.scaffolder.generator import (
40
40
  _write,
41
41
  _config_content,
42
42
  _gitignore,
43
+ _infrakit_dep,
44
+ _pkg_dep,
43
45
  _logger_util,
44
46
  _src_init,
45
47
  _tests_init,
@@ -199,13 +201,21 @@ def test_hello_name():
199
201
  def _cli_pyproject(
200
202
  project_name: str, version: str, description: str, author: str, include_llm: bool
201
203
  ) -> str:
202
- author_line = f' "{author}",' if author else ' # "Your Name <you@example.com>",'
203
- cmd = project_name.replace("_", "-")
204
- llm_deps = """\
205
- "openai",
206
- "google-generativeai",
207
- "tqdm",
208
- """ if include_llm else ""
204
+ author_line = f' "{author}",' if author else ' # "Your Name <you@example.com>",'
205
+ cmd = project_name.replace("_", "-")
206
+ infrakit_dep = _infrakit_dep()
207
+ typer_dep = _pkg_dep("typer")
208
+ pydantic_dep = _pkg_dep("pydantic")
209
+ openai_dep = _pkg_dep("openai")
210
+ genai_dep = _pkg_dep("google-genai")
211
+ groq_dep = _pkg_dep("groq")
212
+ tqdm_dep = _pkg_dep("tqdm")
213
+ llm_deps = f'''\
214
+ "{openai_dep}",
215
+ "{genai_dep}",
216
+ "{groq_dep}",
217
+ "{tqdm_dep}",
218
+ ''' if include_llm else ""
209
219
  return f"""\
210
220
  [project]
211
221
  name = "{project_name}"
@@ -218,9 +228,9 @@ authors = [
218
228
  ]
219
229
 
220
230
  dependencies = [
221
- "infrakit",
222
- "typer[all]",
223
- "pydantic>=2.0",
231
+ {infrakit_dep},
232
+ "{typer_dep}[all]",
233
+ "{pydantic_dep}",
224
234
  {llm_deps}]
225
235
 
226
236
  [project.optional-dependencies]
@@ -63,6 +63,58 @@ def _mkdir(result: ScaffoldResult, path: Path) -> None:
63
63
  result.entries.append(ScaffoldEntry(path=path, status="created", kind="dir"))
64
64
 
65
65
 
66
+ # ── helpers ───────────────────────────────────────────────────────────────────
67
+
68
+ _version_cache: dict[str, str] = {}
69
+
70
+
71
+ def _get_package_version(pkg_name: str) -> str:
72
+ """
73
+ Return the installed or latest-PyPI version of *pkg_name*.
74
+
75
+ Tries ``importlib.metadata`` first (fast, no network), then falls back
76
+ to a PyPI API call. Results are cached in-process.
77
+ """
78
+ if pkg_name in _version_cache:
79
+ return _version_cache[pkg_name]
80
+
81
+ ver = ""
82
+ try:
83
+ from importlib.metadata import version
84
+ ver = version(pkg_name)
85
+ except Exception:
86
+ pass
87
+
88
+ if not ver:
89
+ try:
90
+ import json
91
+ import urllib.request
92
+ url = f"https://pypi.org/pypi/{pkg_name}/json"
93
+ with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310
94
+ ver = json.loads(resp.read())["info"]["version"]
95
+ except Exception:
96
+ pass
97
+
98
+ _version_cache[pkg_name] = ver
99
+ return ver
100
+
101
+
102
+ def _get_infrakit_version() -> str:
103
+ return _get_package_version("python-infrakit-dev")
104
+
105
+
106
+ def _infrakit_dep() -> str:
107
+ """Return the versioned dependency string for python-infrakit-dev."""
108
+ ver = _get_infrakit_version()
109
+ return f'"python-infrakit-dev>={ver}"' if ver else '"python-infrakit-dev"'
110
+
111
+
112
+ def _pkg_dep(pkg_name: str) -> str:
113
+ """Return a versioned dep string like ``openai>=1.2.3`` or just ``openai``."""
114
+ ver = _get_package_version(pkg_name)
115
+ return f"{pkg_name}>={ver}" if ver else pkg_name
116
+
117
+
66
118
  # ── template content ──────────────────────────────────────────────────────────
67
119
 
68
120
  def _pyproject_toml(
@@ -72,6 +124,7 @@ def _pyproject_toml(
72
124
  author: str,
73
125
  ) -> str:
74
126
  author_line = f' "{author}",' if author else ' # "Your Name <you@example.com>",'
127
+ infrakit_dep = _infrakit_dep()
75
128
  return f"""\
76
129
  [project]
77
130
  name = "{project_name}"
@@ -84,7 +137,7 @@ authors = [
84
137
  ]
85
138
 
86
139
  dependencies = [
87
- "infrakit",
140
+ {infrakit_dep},
88
141
  ]
89
142
 
90
143
  [project.optional-dependencies]
@@ -96,10 +149,12 @@ dev = [
96
149
 
97
150
 
98
151
  def _requirements_txt(project_name: str) -> str:
152
+ ver = _get_infrakit_version()
153
+ dep = f"python-infrakit-dev>={ver}" if ver else "python-infrakit-dev"
99
154
  return f"""\
100
155
  # requirements.txt — {project_name}
101
156
  # Add your dependencies below.
102
- infrakit
157
+ {dep}
103
158
  """
104
159
 
105
160
 
@@ -110,10 +165,12 @@ def _env_config(include_llm: bool = False) -> str:
110
165
  LLM_KEYS_FILE=keys.json
111
166
  OPENAI_API_KEY=
112
167
  GEMINI_API_KEY=
168
+ GROQ_API_KEY=
113
169
  LLM_MODE=async
114
170
  LLM_CONCURRENCY=3
115
171
  # OPENAI_MODEL=gpt-4o
116
172
  # GEMINI_MODEL=gemini-2.0-flash
173
+ # GROQ_MODEL=llama-3.3-70b-versatile
117
174
  # LLM_STATE_DIR=
118
175
  # LLM_QUOTA_FILE=
119
176
  """ if include_llm else ""
@@ -139,10 +196,12 @@ def _yaml_config(include_llm: bool = False) -> str:
139
196
  LLM_KEYS_FILE: keys.json
140
197
  OPENAI_API_KEY: ""
141
198
  GEMINI_API_KEY: ""
199
+ GROQ_API_KEY: ""
142
200
  LLM_MODE: async
143
201
  LLM_CONCURRENCY: 3
144
202
  # OPENAI_MODEL: gpt-4o
145
203
  # GEMINI_MODEL: gemini-2.0-flash
204
+ # GROQ_MODEL: llama-3.3-70b-versatile
146
205
  # LLM_STATE_DIR: ""
147
206
  # LLM_QUOTA_FILE: ""
148
207
  """ if include_llm else ""
@@ -167,6 +226,7 @@ def _json_config(include_llm: bool = False) -> str:
167
226
  "LLM_KEYS_FILE": "keys.json",
168
227
  "OPENAI_API_KEY": "",
169
228
  "GEMINI_API_KEY": "",
229
+ "GROQ_API_KEY": "",
170
230
  "LLM_MODE": "async",
171
231
  "LLM_CONCURRENCY": 3""" if include_llm else ""
172
232
  return f"""\
@@ -49,6 +49,8 @@ from infrakit.scaffolder.generator import (
49
49
  _write,
50
50
  _config_content,
51
51
  _gitignore,
52
+ _infrakit_dep,
53
+ _pkg_dep,
52
54
  _logger_util,
53
55
  _src_init,
54
56
  _tests_init,
@@ -374,12 +376,19 @@ def test_runner_returns_all_stages():
374
376
  def _pipeline_pyproject(
375
377
  project_name: str, version: str, description: str, author: str, include_llm: bool
376
378
  ) -> str:
377
- author_line = f' "{author}",' if author else ' # "Your Name <you@example.com>",'
378
- llm_deps = """\
379
- "openai",
380
- "google-generativeai",
381
- "tqdm",
382
- """ if include_llm else ""
379
+ author_line = f' "{author}",' if author else ' # "Your Name <you@example.com>",'
380
+ infrakit_dep = _infrakit_dep()
381
+ pydantic_dep = _pkg_dep("pydantic")
382
+ openai_dep = _pkg_dep("openai")
383
+ genai_dep = _pkg_dep("google-genai")
384
+ groq_dep = _pkg_dep("groq")
385
+ tqdm_dep = _pkg_dep("tqdm")
386
+ llm_deps = f'''\
387
+ "{openai_dep}",
388
+ "{genai_dep}",
389
+ "{groq_dep}",
390
+ "{tqdm_dep}",
391
+ ''' if include_llm else ""
383
392
  return f"""\
384
393
  [project]
385
394
  name = "{project_name}"
@@ -392,8 +401,8 @@ authors = [
392
401
  ]
393
402
 
394
403
  dependencies = [
395
- "infrakit",
396
- "pydantic>=2.0",
404
+ {infrakit_dep},
405
+ "{pydantic_dep}",
397
406
  {llm_deps}]
398
407
 
399
408
  [project.optional-dependencies]