parseforge 0.2.5__tar.gz → 0.2.6__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.
- {parseforge-0.2.5/parseforge.egg-info → parseforge-0.2.6}/PKG-INFO +36 -9
- {parseforge-0.2.5 → parseforge-0.2.6}/README.md +11 -8
- parseforge-0.2.6/parseforge/__init__.py +1 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/cli/main.py +15 -4
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/__init__.py +21 -1
- parseforge-0.2.6/parseforge/naming/providers/__init__.py +25 -0
- parseforge-0.2.6/parseforge/naming/providers/cerebras.py +128 -0
- parseforge-0.2.6/parseforge/naming/providers/fireworks.py +128 -0
- parseforge-0.2.6/parseforge/naming/providers/groq.py +126 -0
- parseforge-0.2.6/parseforge/naming/providers/models.yaml +112 -0
- parseforge-0.2.6/parseforge/naming/providers/moonshot.py +130 -0
- parseforge-0.2.6/parseforge/naming/providers/openrouter.py +130 -0
- parseforge-0.2.6/parseforge/naming/providers/perplexity.py +128 -0
- parseforge-0.2.6/parseforge/naming/providers/together.py +128 -0
- parseforge-0.2.6/parseforge/naming/providers/xai.py +126 -0
- {parseforge-0.2.5 → parseforge-0.2.6/parseforge.egg-info}/PKG-INFO +36 -9
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge.egg-info/SOURCES.txt +8 -0
- parseforge-0.2.6/parseforge.egg-info/requires.txt +64 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/pyproject.toml +9 -1
- parseforge-0.2.5/parseforge/__init__.py +0 -1
- parseforge-0.2.5/parseforge/naming/providers/__init__.py +0 -5
- parseforge-0.2.5/parseforge/naming/providers/models.yaml +0 -33
- parseforge-0.2.5/parseforge.egg-info/requires.txt +0 -32
- {parseforge-0.2.5 → parseforge-0.2.6}/LICENSE +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/MANIFEST.in +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/SPEC.md +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/cli/__init__.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/cli/config.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/drift.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/generation.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/integration.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/assemble.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/cache.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/llm.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/prompts.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/prompts.yaml +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/providers/anthropic.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/providers/cost.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/providers/deepseek.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/providers/errors.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/providers/models.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/providers/openai.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/providers/text.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/naming/resolver.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/paths.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/pipeline.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/promotion.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/sampling/__init__.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/sampling/backends/__init__.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/sampling/backends/netmiko.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/sampling/core.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge/validation.py +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge.egg-info/dependency_links.txt +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge.egg-info/entry_points.txt +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/parseforge.egg-info/top_level.txt +0 -0
- {parseforge-0.2.5 → parseforge-0.2.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: parseforge
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: LLM-driven pipeline that forges, validates, and promotes TextFSM templates from network CLI output
|
|
5
5
|
Author-email: Tuyen Mathew Duong <tuyen@geekstrident.com>
|
|
6
6
|
Maintainer-email: Tuyen Mathew Duong <tuyen@geekstrident.com>
|
|
@@ -36,6 +36,30 @@ Requires-Dist: textfsm-ai[deepseek]>=0.6.1; extra == "deepseek"
|
|
|
36
36
|
Provides-Extra: openai
|
|
37
37
|
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
38
38
|
Requires-Dist: textfsm-ai[openai]>=0.6.1; extra == "openai"
|
|
39
|
+
Provides-Extra: groq
|
|
40
|
+
Requires-Dist: openai>=1.0.0; extra == "groq"
|
|
41
|
+
Requires-Dist: textfsm-ai[groq]>=0.6.1; extra == "groq"
|
|
42
|
+
Provides-Extra: xai
|
|
43
|
+
Requires-Dist: openai>=1.0.0; extra == "xai"
|
|
44
|
+
Requires-Dist: textfsm-ai[xai]>=0.6.1; extra == "xai"
|
|
45
|
+
Provides-Extra: together
|
|
46
|
+
Requires-Dist: openai>=1.0.0; extra == "together"
|
|
47
|
+
Requires-Dist: textfsm-ai[together]>=0.6.1; extra == "together"
|
|
48
|
+
Provides-Extra: fireworks
|
|
49
|
+
Requires-Dist: openai>=1.0.0; extra == "fireworks"
|
|
50
|
+
Requires-Dist: textfsm-ai[fireworks]>=0.6.1; extra == "fireworks"
|
|
51
|
+
Provides-Extra: perplexity
|
|
52
|
+
Requires-Dist: openai>=1.0.0; extra == "perplexity"
|
|
53
|
+
Requires-Dist: textfsm-ai[perplexity]>=0.6.1; extra == "perplexity"
|
|
54
|
+
Provides-Extra: openrouter
|
|
55
|
+
Requires-Dist: openai>=1.0.0; extra == "openrouter"
|
|
56
|
+
Requires-Dist: textfsm-ai[openrouter]>=0.6.1; extra == "openrouter"
|
|
57
|
+
Provides-Extra: moonshot
|
|
58
|
+
Requires-Dist: openai>=1.0.0; extra == "moonshot"
|
|
59
|
+
Requires-Dist: textfsm-ai[moonshot]>=0.6.1; extra == "moonshot"
|
|
60
|
+
Provides-Extra: cerebras
|
|
61
|
+
Requires-Dist: openai>=1.0.0; extra == "cerebras"
|
|
62
|
+
Requires-Dist: textfsm-ai[cerebras]>=0.6.1; extra == "cerebras"
|
|
39
63
|
Provides-Extra: sampling
|
|
40
64
|
Requires-Dist: netmiko>=4.0; extra == "sampling"
|
|
41
65
|
Provides-Extra: dev
|
|
@@ -86,11 +110,13 @@ pip install parseforge[anthropic]
|
|
|
86
110
|
command that's pure local processing (`canonical`/`readable`/`recognizers`,
|
|
87
111
|
`integration`, `promotion`) works with nothing further. Anything that calls an
|
|
88
112
|
LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
|
|
89
|
-
extra for whichever provider it uses: `anthropic`, `openai`,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`
|
|
113
|
+
extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
|
|
114
|
+
`groq`, `xai`, `together`, `fireworks`, `perplexity`, `openrouter`,
|
|
115
|
+
`moonshot`, or `cerebras`. `--provider` defaults to `anthropic` wherever it
|
|
116
|
+
isn't required, so that's the one most setups need. `pip install
|
|
117
|
+
parseforge[sampling]` adds Netmiko for live device sampling; combine extras
|
|
118
|
+
as needed, e.g.
|
|
119
|
+
`pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,sampling]`.
|
|
94
120
|
|
|
95
121
|
## Development
|
|
96
122
|
|
|
@@ -99,9 +125,10 @@ pip install -e ".[dev,sampling]"
|
|
|
99
125
|
pytest
|
|
100
126
|
```
|
|
101
127
|
`dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
|
|
102
|
-
all
|
|
103
|
-
|
|
104
|
-
|
|
128
|
+
all eleven providers — `anthropic`, `openai`, `deepseek`, `groq`, `xai`,
|
|
129
|
+
`together`, `fireworks`, `perplexity`, `openrouter`, `moonshot`, `cerebras`
|
|
130
|
+
share just those two packages — and never silently skip) — add the specific
|
|
131
|
+
`,<provider>` extra explicitly only if installing outside of `dev`.
|
|
105
132
|
|
|
106
133
|
Linting/formatting/type-checking/docs run through tox instead of extras — see
|
|
107
134
|
`tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
|
|
@@ -33,11 +33,13 @@ pip install parseforge[anthropic]
|
|
|
33
33
|
command that's pure local processing (`canonical`/`readable`/`recognizers`,
|
|
34
34
|
`integration`, `promotion`) works with nothing further. Anything that calls an
|
|
35
35
|
LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
|
|
36
|
-
extra for whichever provider it uses: `anthropic`, `openai`,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
`
|
|
36
|
+
extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
|
|
37
|
+
`groq`, `xai`, `together`, `fireworks`, `perplexity`, `openrouter`,
|
|
38
|
+
`moonshot`, or `cerebras`. `--provider` defaults to `anthropic` wherever it
|
|
39
|
+
isn't required, so that's the one most setups need. `pip install
|
|
40
|
+
parseforge[sampling]` adds Netmiko for live device sampling; combine extras
|
|
41
|
+
as needed, e.g.
|
|
42
|
+
`pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,sampling]`.
|
|
41
43
|
|
|
42
44
|
## Development
|
|
43
45
|
|
|
@@ -46,9 +48,10 @@ pip install -e ".[dev,sampling]"
|
|
|
46
48
|
pytest
|
|
47
49
|
```
|
|
48
50
|
`dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
|
|
49
|
-
all
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
all eleven providers — `anthropic`, `openai`, `deepseek`, `groq`, `xai`,
|
|
52
|
+
`together`, `fireworks`, `perplexity`, `openrouter`, `moonshot`, `cerebras`
|
|
53
|
+
share just those two packages — and never silently skip) — add the specific
|
|
54
|
+
`,<provider>` extra explicitly only if installing outside of `dev`.
|
|
52
55
|
|
|
53
56
|
Linting/formatting/type-checking/docs run through tox instead of extras — see
|
|
54
57
|
`tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.6"
|
|
@@ -23,8 +23,16 @@ from parseforge.cli import config as cli_config
|
|
|
23
23
|
|
|
24
24
|
_BUILDERS: dict[str, type[naming.RegexBuilder]] = {
|
|
25
25
|
"anthropic": naming.AnthropicRegexBuilder,
|
|
26
|
+
"cerebras": naming.CerebrasRegexBuilder,
|
|
26
27
|
"deepseek": naming.DeepSeekRegexBuilder,
|
|
28
|
+
"fireworks": naming.FireworksRegexBuilder,
|
|
29
|
+
"groq": naming.GroqRegexBuilder,
|
|
30
|
+
"moonshot": naming.MoonshotRegexBuilder,
|
|
27
31
|
"openai": naming.OpenAIRegexBuilder,
|
|
32
|
+
"openrouter": naming.OpenRouterRegexBuilder,
|
|
33
|
+
"perplexity": naming.PerplexityRegexBuilder,
|
|
34
|
+
"together": naming.TogetherRegexBuilder,
|
|
35
|
+
"xai": naming.XAIRegexBuilder,
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
_CONNECTORS = ("netmiko",)
|
|
@@ -199,8 +207,10 @@ def main() -> None:
|
|
|
199
207
|
"--api-key",
|
|
200
208
|
default=None,
|
|
201
209
|
help="Provider API key. Defaults to that provider's own API key environment "
|
|
202
|
-
"variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY
|
|
203
|
-
"
|
|
210
|
+
"variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GROQ_API_KEY, "
|
|
211
|
+
"XAI_API_KEY, TOGETHER_API_KEY, FIREWORKS_API_KEY, PERPLEXITY_API_KEY, "
|
|
212
|
+
"OPENROUTER_API_KEY, MOONSHOT_API_KEY, CEREBRAS_API_KEY); only needed on a "
|
|
213
|
+
"cache miss.",
|
|
204
214
|
)
|
|
205
215
|
@click.option(
|
|
206
216
|
"--model",
|
|
@@ -268,8 +278,9 @@ def name_cmd(
|
|
|
268
278
|
"--provider",
|
|
269
279
|
required=True,
|
|
270
280
|
help="LLM provider for template generation (textfsm-ai's own registry, "
|
|
271
|
-
'e.g. "anthropic", "openai", "deepseek"
|
|
272
|
-
"
|
|
281
|
+
'e.g. "anthropic", "openai", "deepseek", "groq", "xai", "together", '
|
|
282
|
+
'"fireworks", "perplexity", "openrouter", "moonshot", "cerebras"). Naming '
|
|
283
|
+
"uses its own separate --naming-provider, not this one.",
|
|
273
284
|
)
|
|
274
285
|
@click.option("--api-key", required=True, help="API key for the generation LLM call.")
|
|
275
286
|
@click.option("--model", required=True, help="Model for the generation LLM call.")
|
|
@@ -8,7 +8,19 @@ from .llm import (
|
|
|
8
8
|
UnimplementedRegexBuilder,
|
|
9
9
|
build_prompt,
|
|
10
10
|
)
|
|
11
|
-
from .providers import
|
|
11
|
+
from .providers import (
|
|
12
|
+
AnthropicRegexBuilder,
|
|
13
|
+
CerebrasRegexBuilder,
|
|
14
|
+
DeepSeekRegexBuilder,
|
|
15
|
+
FireworksRegexBuilder,
|
|
16
|
+
GroqRegexBuilder,
|
|
17
|
+
MoonshotRegexBuilder,
|
|
18
|
+
OpenAIRegexBuilder,
|
|
19
|
+
OpenRouterRegexBuilder,
|
|
20
|
+
PerplexityRegexBuilder,
|
|
21
|
+
TogetherRegexBuilder,
|
|
22
|
+
XAIRegexBuilder,
|
|
23
|
+
)
|
|
12
24
|
from .resolver import NamingResolution, cli_name, resolve_cli_name
|
|
13
25
|
|
|
14
26
|
__all__ = [
|
|
@@ -19,8 +31,16 @@ __all__ = [
|
|
|
19
31
|
"RegexBuilder",
|
|
20
32
|
"UnimplementedRegexBuilder",
|
|
21
33
|
"AnthropicRegexBuilder",
|
|
34
|
+
"CerebrasRegexBuilder",
|
|
22
35
|
"DeepSeekRegexBuilder",
|
|
36
|
+
"FireworksRegexBuilder",
|
|
37
|
+
"GroqRegexBuilder",
|
|
38
|
+
"MoonshotRegexBuilder",
|
|
23
39
|
"OpenAIRegexBuilder",
|
|
40
|
+
"OpenRouterRegexBuilder",
|
|
41
|
+
"PerplexityRegexBuilder",
|
|
42
|
+
"TogetherRegexBuilder",
|
|
43
|
+
"XAIRegexBuilder",
|
|
24
44
|
"LLMCLIResponse",
|
|
25
45
|
"TokenUsage",
|
|
26
46
|
"build_prompt",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .anthropic import AnthropicRegexBuilder
|
|
2
|
+
from .cerebras import CerebrasRegexBuilder
|
|
3
|
+
from .deepseek import DeepSeekRegexBuilder
|
|
4
|
+
from .fireworks import FireworksRegexBuilder
|
|
5
|
+
from .groq import GroqRegexBuilder
|
|
6
|
+
from .moonshot import MoonshotRegexBuilder
|
|
7
|
+
from .openai import OpenAIRegexBuilder
|
|
8
|
+
from .openrouter import OpenRouterRegexBuilder
|
|
9
|
+
from .perplexity import PerplexityRegexBuilder
|
|
10
|
+
from .together import TogetherRegexBuilder
|
|
11
|
+
from .xai import XAIRegexBuilder
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AnthropicRegexBuilder",
|
|
15
|
+
"CerebrasRegexBuilder",
|
|
16
|
+
"DeepSeekRegexBuilder",
|
|
17
|
+
"FireworksRegexBuilder",
|
|
18
|
+
"GroqRegexBuilder",
|
|
19
|
+
"MoonshotRegexBuilder",
|
|
20
|
+
"OpenAIRegexBuilder",
|
|
21
|
+
"OpenRouterRegexBuilder",
|
|
22
|
+
"PerplexityRegexBuilder",
|
|
23
|
+
"TogetherRegexBuilder",
|
|
24
|
+
"XAIRegexBuilder",
|
|
25
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Cerebras-backed RegexBuilder implementation.
|
|
2
|
+
|
|
3
|
+
Cerebras' API mirrors OpenAI's chat.completions surface (same request/
|
|
4
|
+
response format), so this uses the `openai` SDK pointed at Cerebras'
|
|
5
|
+
base URL rather than a dedicated Cerebras client.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
|
|
15
|
+
from .cost import estimate_cost
|
|
16
|
+
from .errors import format_llm_error_reason, is_retryable
|
|
17
|
+
from .models import default_model
|
|
18
|
+
from .text import extract_pattern
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import openai as openai_sdk
|
|
22
|
+
|
|
23
|
+
DEFAULT_MODEL = default_model("cerebras")
|
|
24
|
+
|
|
25
|
+
_BASE_URL = "https://api.cerebras.ai/v1"
|
|
26
|
+
|
|
27
|
+
_DEFAULT_MAX_TOKENS = 1024
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _import_openai() -> Any:
|
|
31
|
+
"""Deferred import — openai is an optional extra (parseforge[cerebras]);
|
|
32
|
+
nothing else in this module (or a cache-hit lookup, which never reaches
|
|
33
|
+
build_pattern at all — see resolver.cli_name) should require it to be
|
|
34
|
+
installed."""
|
|
35
|
+
try:
|
|
36
|
+
import openai
|
|
37
|
+
except ImportError as exc:
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"the openai package is required to use CerebrasRegexBuilder — "
|
|
40
|
+
"install it via `pip install parseforge[cerebras]`"
|
|
41
|
+
) from exc
|
|
42
|
+
return openai
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CerebrasRegexBuilder:
|
|
46
|
+
"""Builds a cli-name regex pattern by prompting a Cerebras-hosted model.
|
|
47
|
+
|
|
48
|
+
The client is constructed lazily, on the first actual call — not in
|
|
49
|
+
``__init__`` — so this can be used as a default RegexBuilder without
|
|
50
|
+
requiring ``CEREBRAS_API_KEY`` to be set for cache-hit lookups, which
|
|
51
|
+
never reach the LLM at all (see resolver.cli_name).
|
|
52
|
+
|
|
53
|
+
API key resolves from the ``api_key`` argument if given, otherwise the
|
|
54
|
+
``CEREBRAS_API_KEY`` environment variable (unlike Anthropic/OpenAI's
|
|
55
|
+
own SDKs, the ``openai`` package has no built-in notion of Cerebras'
|
|
56
|
+
key, so this is resolved explicitly rather than left to the SDK).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
provider = "cerebras"
|
|
60
|
+
|
|
61
|
+
def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
|
|
62
|
+
self.model = model
|
|
63
|
+
self._api_key = api_key
|
|
64
|
+
self._client: openai_sdk.OpenAI | None = None
|
|
65
|
+
|
|
66
|
+
def _get_client(self) -> openai_sdk.OpenAI:
|
|
67
|
+
if self._client is None:
|
|
68
|
+
openai = _import_openai()
|
|
69
|
+
api_key = self._api_key or os.environ.get("CEREBRAS_API_KEY")
|
|
70
|
+
if not api_key:
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
"no Cerebras API key — pass api_key or set CEREBRAS_API_KEY"
|
|
73
|
+
)
|
|
74
|
+
self._client = openai.OpenAI(api_key=api_key, base_url=_BASE_URL)
|
|
75
|
+
return self._client
|
|
76
|
+
|
|
77
|
+
def build_pattern(
|
|
78
|
+
self, command: str, context: CliContext, **kwargs: Any
|
|
79
|
+
) -> LLMCLIResponse:
|
|
80
|
+
openai = _import_openai()
|
|
81
|
+
prompt = build_prompt(command, context)
|
|
82
|
+
max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
|
|
83
|
+
|
|
84
|
+
start = time.monotonic()
|
|
85
|
+
try:
|
|
86
|
+
response = self._get_client().chat.completions.create(
|
|
87
|
+
model=self.model,
|
|
88
|
+
max_tokens=max_tokens,
|
|
89
|
+
messages=[{"role": "user", "content": prompt}],
|
|
90
|
+
**kwargs,
|
|
91
|
+
)
|
|
92
|
+
except openai.OpenAIError as exc:
|
|
93
|
+
if not is_retryable(exc):
|
|
94
|
+
# Same request would fail the same way again — stop rather
|
|
95
|
+
# than let a caller burn another attempt on it.
|
|
96
|
+
raise
|
|
97
|
+
return LLMCLIResponse(
|
|
98
|
+
content="",
|
|
99
|
+
raw=exc,
|
|
100
|
+
usage=TokenUsage(
|
|
101
|
+
input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
|
|
102
|
+
),
|
|
103
|
+
duration_ms=(time.monotonic() - start) * 1000,
|
|
104
|
+
reason=format_llm_error_reason(exc),
|
|
105
|
+
ready=False,
|
|
106
|
+
)
|
|
107
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
108
|
+
|
|
109
|
+
choice = response.choices[0]
|
|
110
|
+
return LLMCLIResponse(
|
|
111
|
+
content=extract_pattern(choice.message.content or ""),
|
|
112
|
+
raw=response,
|
|
113
|
+
usage=TokenUsage(
|
|
114
|
+
input_tokens=response.usage.prompt_tokens,
|
|
115
|
+
output_tokens=response.usage.completion_tokens,
|
|
116
|
+
total_tokens=response.usage.total_tokens,
|
|
117
|
+
estimated_cost=estimate_cost(
|
|
118
|
+
input_tokens=response.usage.prompt_tokens,
|
|
119
|
+
output_tokens=response.usage.completion_tokens,
|
|
120
|
+
total_tokens=response.usage.total_tokens,
|
|
121
|
+
provider=self.provider,
|
|
122
|
+
model=self.model,
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
duration_ms=duration_ms,
|
|
126
|
+
reason=choice.finish_reason or "",
|
|
127
|
+
ready=choice.finish_reason == "stop",
|
|
128
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Fireworks AI-backed RegexBuilder implementation.
|
|
2
|
+
|
|
3
|
+
Fireworks' API mirrors OpenAI's chat.completions surface (same request/
|
|
4
|
+
response format), so this uses the `openai` SDK pointed at Fireworks'
|
|
5
|
+
base URL rather than a dedicated Fireworks client.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
|
|
15
|
+
from .cost import estimate_cost
|
|
16
|
+
from .errors import format_llm_error_reason, is_retryable
|
|
17
|
+
from .models import default_model
|
|
18
|
+
from .text import extract_pattern
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import openai as openai_sdk
|
|
22
|
+
|
|
23
|
+
DEFAULT_MODEL = default_model("fireworks")
|
|
24
|
+
|
|
25
|
+
_BASE_URL = "https://api.fireworks.ai/inference/v1"
|
|
26
|
+
|
|
27
|
+
_DEFAULT_MAX_TOKENS = 1024
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _import_openai() -> Any:
|
|
31
|
+
"""Deferred import — openai is an optional extra (parseforge[fireworks]);
|
|
32
|
+
nothing else in this module (or a cache-hit lookup, which never reaches
|
|
33
|
+
build_pattern at all — see resolver.cli_name) should require it to be
|
|
34
|
+
installed."""
|
|
35
|
+
try:
|
|
36
|
+
import openai
|
|
37
|
+
except ImportError as exc:
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"the openai package is required to use FireworksRegexBuilder — "
|
|
40
|
+
"install it via `pip install parseforge[fireworks]`"
|
|
41
|
+
) from exc
|
|
42
|
+
return openai
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FireworksRegexBuilder:
|
|
46
|
+
"""Builds a cli-name regex pattern by prompting a Fireworks-hosted model.
|
|
47
|
+
|
|
48
|
+
The client is constructed lazily, on the first actual call — not in
|
|
49
|
+
``__init__`` — so this can be used as a default RegexBuilder without
|
|
50
|
+
requiring ``FIREWORKS_API_KEY`` to be set for cache-hit lookups, which
|
|
51
|
+
never reach the LLM at all (see resolver.cli_name).
|
|
52
|
+
|
|
53
|
+
API key resolves from the ``api_key`` argument if given, otherwise the
|
|
54
|
+
``FIREWORKS_API_KEY`` environment variable (unlike Anthropic/OpenAI's
|
|
55
|
+
own SDKs, the ``openai`` package has no built-in notion of Fireworks'
|
|
56
|
+
key, so this is resolved explicitly rather than left to the SDK).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
provider = "fireworks"
|
|
60
|
+
|
|
61
|
+
def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
|
|
62
|
+
self.model = model
|
|
63
|
+
self._api_key = api_key
|
|
64
|
+
self._client: openai_sdk.OpenAI | None = None
|
|
65
|
+
|
|
66
|
+
def _get_client(self) -> openai_sdk.OpenAI:
|
|
67
|
+
if self._client is None:
|
|
68
|
+
openai = _import_openai()
|
|
69
|
+
api_key = self._api_key or os.environ.get("FIREWORKS_API_KEY")
|
|
70
|
+
if not api_key:
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
"no Fireworks API key — pass api_key or set FIREWORKS_API_KEY"
|
|
73
|
+
)
|
|
74
|
+
self._client = openai.OpenAI(api_key=api_key, base_url=_BASE_URL)
|
|
75
|
+
return self._client
|
|
76
|
+
|
|
77
|
+
def build_pattern(
|
|
78
|
+
self, command: str, context: CliContext, **kwargs: Any
|
|
79
|
+
) -> LLMCLIResponse:
|
|
80
|
+
openai = _import_openai()
|
|
81
|
+
prompt = build_prompt(command, context)
|
|
82
|
+
max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
|
|
83
|
+
|
|
84
|
+
start = time.monotonic()
|
|
85
|
+
try:
|
|
86
|
+
response = self._get_client().chat.completions.create(
|
|
87
|
+
model=self.model,
|
|
88
|
+
max_tokens=max_tokens,
|
|
89
|
+
messages=[{"role": "user", "content": prompt}],
|
|
90
|
+
**kwargs,
|
|
91
|
+
)
|
|
92
|
+
except openai.OpenAIError as exc:
|
|
93
|
+
if not is_retryable(exc):
|
|
94
|
+
# Same request would fail the same way again — stop rather
|
|
95
|
+
# than let a caller burn another attempt on it.
|
|
96
|
+
raise
|
|
97
|
+
return LLMCLIResponse(
|
|
98
|
+
content="",
|
|
99
|
+
raw=exc,
|
|
100
|
+
usage=TokenUsage(
|
|
101
|
+
input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
|
|
102
|
+
),
|
|
103
|
+
duration_ms=(time.monotonic() - start) * 1000,
|
|
104
|
+
reason=format_llm_error_reason(exc),
|
|
105
|
+
ready=False,
|
|
106
|
+
)
|
|
107
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
108
|
+
|
|
109
|
+
choice = response.choices[0]
|
|
110
|
+
return LLMCLIResponse(
|
|
111
|
+
content=extract_pattern(choice.message.content or ""),
|
|
112
|
+
raw=response,
|
|
113
|
+
usage=TokenUsage(
|
|
114
|
+
input_tokens=response.usage.prompt_tokens,
|
|
115
|
+
output_tokens=response.usage.completion_tokens,
|
|
116
|
+
total_tokens=response.usage.total_tokens,
|
|
117
|
+
estimated_cost=estimate_cost(
|
|
118
|
+
input_tokens=response.usage.prompt_tokens,
|
|
119
|
+
output_tokens=response.usage.completion_tokens,
|
|
120
|
+
total_tokens=response.usage.total_tokens,
|
|
121
|
+
provider=self.provider,
|
|
122
|
+
model=self.model,
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
duration_ms=duration_ms,
|
|
126
|
+
reason=choice.finish_reason or "",
|
|
127
|
+
ready=choice.finish_reason == "stop",
|
|
128
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Groq-backed RegexBuilder implementation.
|
|
2
|
+
|
|
3
|
+
Groq's API mirrors OpenAI's chat.completions surface (same request/
|
|
4
|
+
response format), so this uses the `openai` SDK pointed at Groq's base
|
|
5
|
+
URL rather than a dedicated Groq client.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
|
|
15
|
+
from .cost import estimate_cost
|
|
16
|
+
from .errors import format_llm_error_reason, is_retryable
|
|
17
|
+
from .models import default_model
|
|
18
|
+
from .text import extract_pattern
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import openai as openai_sdk
|
|
22
|
+
|
|
23
|
+
DEFAULT_MODEL = default_model("groq")
|
|
24
|
+
|
|
25
|
+
_BASE_URL = "https://api.groq.com/openai/v1"
|
|
26
|
+
|
|
27
|
+
_DEFAULT_MAX_TOKENS = 1024
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _import_openai() -> Any:
|
|
31
|
+
"""Deferred import — openai is an optional extra (parseforge[groq]);
|
|
32
|
+
nothing else in this module (or a cache-hit lookup, which never reaches
|
|
33
|
+
build_pattern at all — see resolver.cli_name) should require it to be
|
|
34
|
+
installed."""
|
|
35
|
+
try:
|
|
36
|
+
import openai
|
|
37
|
+
except ImportError as exc:
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"the openai package is required to use GroqRegexBuilder — "
|
|
40
|
+
"install it via `pip install parseforge[groq]`"
|
|
41
|
+
) from exc
|
|
42
|
+
return openai
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GroqRegexBuilder:
|
|
46
|
+
"""Builds a cli-name regex pattern by prompting a Groq-hosted model.
|
|
47
|
+
|
|
48
|
+
The client is constructed lazily, on the first actual call — not in
|
|
49
|
+
``__init__`` — so this can be used as a default RegexBuilder without
|
|
50
|
+
requiring ``GROQ_API_KEY`` to be set for cache-hit lookups, which
|
|
51
|
+
never reach the LLM at all (see resolver.cli_name).
|
|
52
|
+
|
|
53
|
+
API key resolves from the ``api_key`` argument if given, otherwise the
|
|
54
|
+
``GROQ_API_KEY`` environment variable (unlike Anthropic/OpenAI's own
|
|
55
|
+
SDKs, the ``openai`` package has no built-in notion of Groq's key, so
|
|
56
|
+
this is resolved explicitly rather than left to the SDK).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
provider = "groq"
|
|
60
|
+
|
|
61
|
+
def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
|
|
62
|
+
self.model = model
|
|
63
|
+
self._api_key = api_key
|
|
64
|
+
self._client: openai_sdk.OpenAI | None = None
|
|
65
|
+
|
|
66
|
+
def _get_client(self) -> openai_sdk.OpenAI:
|
|
67
|
+
if self._client is None:
|
|
68
|
+
openai = _import_openai()
|
|
69
|
+
api_key = self._api_key or os.environ.get("GROQ_API_KEY")
|
|
70
|
+
if not api_key:
|
|
71
|
+
raise RuntimeError("no Groq API key — pass api_key or set GROQ_API_KEY")
|
|
72
|
+
self._client = openai.OpenAI(api_key=api_key, base_url=_BASE_URL)
|
|
73
|
+
return self._client
|
|
74
|
+
|
|
75
|
+
def build_pattern(
|
|
76
|
+
self, command: str, context: CliContext, **kwargs: Any
|
|
77
|
+
) -> LLMCLIResponse:
|
|
78
|
+
openai = _import_openai()
|
|
79
|
+
prompt = build_prompt(command, context)
|
|
80
|
+
max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
|
|
81
|
+
|
|
82
|
+
start = time.monotonic()
|
|
83
|
+
try:
|
|
84
|
+
response = self._get_client().chat.completions.create(
|
|
85
|
+
model=self.model,
|
|
86
|
+
max_tokens=max_tokens,
|
|
87
|
+
messages=[{"role": "user", "content": prompt}],
|
|
88
|
+
**kwargs,
|
|
89
|
+
)
|
|
90
|
+
except openai.OpenAIError as exc:
|
|
91
|
+
if not is_retryable(exc):
|
|
92
|
+
# Same request would fail the same way again — stop rather
|
|
93
|
+
# than let a caller burn another attempt on it.
|
|
94
|
+
raise
|
|
95
|
+
return LLMCLIResponse(
|
|
96
|
+
content="",
|
|
97
|
+
raw=exc,
|
|
98
|
+
usage=TokenUsage(
|
|
99
|
+
input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
|
|
100
|
+
),
|
|
101
|
+
duration_ms=(time.monotonic() - start) * 1000,
|
|
102
|
+
reason=format_llm_error_reason(exc),
|
|
103
|
+
ready=False,
|
|
104
|
+
)
|
|
105
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
106
|
+
|
|
107
|
+
choice = response.choices[0]
|
|
108
|
+
return LLMCLIResponse(
|
|
109
|
+
content=extract_pattern(choice.message.content or ""),
|
|
110
|
+
raw=response,
|
|
111
|
+
usage=TokenUsage(
|
|
112
|
+
input_tokens=response.usage.prompt_tokens,
|
|
113
|
+
output_tokens=response.usage.completion_tokens,
|
|
114
|
+
total_tokens=response.usage.total_tokens,
|
|
115
|
+
estimated_cost=estimate_cost(
|
|
116
|
+
input_tokens=response.usage.prompt_tokens,
|
|
117
|
+
output_tokens=response.usage.completion_tokens,
|
|
118
|
+
total_tokens=response.usage.total_tokens,
|
|
119
|
+
provider=self.provider,
|
|
120
|
+
model=self.model,
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
duration_ms=duration_ms,
|
|
124
|
+
reason=choice.finish_reason or "",
|
|
125
|
+
ready=choice.finish_reason == "stop",
|
|
126
|
+
)
|