parseforge 0.2.3__tar.gz → 0.2.5__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.3/parseforge.egg-info → parseforge-0.2.5}/PKG-INFO +42 -6
- {parseforge-0.2.3 → parseforge-0.2.5}/README.md +26 -2
- parseforge-0.2.5/parseforge/__init__.py +1 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/cli/main.py +4 -2
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/generation.py +6 -1
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/__init__.py +2 -1
- parseforge-0.2.5/parseforge/naming/providers/__init__.py +5 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/anthropic.py +27 -7
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/deepseek.py +25 -7
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/models.yaml +9 -0
- parseforge-0.2.5/parseforge/naming/providers/openai.py +117 -0
- {parseforge-0.2.3 → parseforge-0.2.5/parseforge.egg-info}/PKG-INFO +42 -6
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge.egg-info/SOURCES.txt +1 -0
- parseforge-0.2.5/parseforge.egg-info/requires.txt +32 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/pyproject.toml +23 -4
- parseforge-0.2.3/parseforge/__init__.py +0 -1
- parseforge-0.2.3/parseforge/naming/providers/__init__.py +0 -4
- parseforge-0.2.3/parseforge.egg-info/requires.txt +0 -16
- {parseforge-0.2.3 → parseforge-0.2.5}/LICENSE +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/MANIFEST.in +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/SPEC.md +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/cli/__init__.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/cli/config.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/drift.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/integration.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/assemble.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/cache.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/llm.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/prompts.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/prompts.yaml +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/cost.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/errors.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/models.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/text.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/resolver.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/paths.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/pipeline.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/promotion.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/sampling/__init__.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/sampling/backends/__init__.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/sampling/backends/netmiko.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/sampling/core.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/validation.py +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge.egg-info/dependency_links.txt +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge.egg-info/entry_points.txt +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/parseforge.egg-info/top_level.txt +0 -0
- {parseforge-0.2.3 → parseforge-0.2.5}/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.5
|
|
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>
|
|
@@ -26,9 +26,16 @@ License-File: LICENSE
|
|
|
26
26
|
Requires-Dist: click>=8.1
|
|
27
27
|
Requires-Dist: PyYAML>=6.0
|
|
28
28
|
Requires-Dist: textfsm>=1.1.0
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
|
|
31
|
-
Requires-Dist:
|
|
29
|
+
Requires-Dist: textfsm-ai>=0.6.1
|
|
30
|
+
Provides-Extra: anthropic
|
|
31
|
+
Requires-Dist: anthropic>=0.40.0; extra == "anthropic"
|
|
32
|
+
Requires-Dist: textfsm-ai[anthropic]>=0.6.1; extra == "anthropic"
|
|
33
|
+
Provides-Extra: deepseek
|
|
34
|
+
Requires-Dist: openai>=1.0.0; extra == "deepseek"
|
|
35
|
+
Requires-Dist: textfsm-ai[deepseek]>=0.6.1; extra == "deepseek"
|
|
36
|
+
Provides-Extra: openai
|
|
37
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
38
|
+
Requires-Dist: textfsm-ai[openai]>=0.6.1; extra == "openai"
|
|
32
39
|
Provides-Extra: sampling
|
|
33
40
|
Requires-Dist: netmiko>=4.0; extra == "sampling"
|
|
34
41
|
Provides-Extra: dev
|
|
@@ -37,6 +44,11 @@ Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
|
37
44
|
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
38
45
|
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
39
46
|
Requires-Dist: mypy>=1.10.0; extra == "dev"
|
|
47
|
+
Requires-Dist: anthropic>=0.40.0; extra == "dev"
|
|
48
|
+
Requires-Dist: openai>=1.0.0; extra == "dev"
|
|
49
|
+
Provides-Extra: release
|
|
50
|
+
Requires-Dist: bump2version; extra == "release"
|
|
51
|
+
Requires-Dist: build; extra == "release"
|
|
40
52
|
Dynamic: license-file
|
|
41
53
|
|
|
42
54
|
# ParseForge
|
|
@@ -65,12 +77,36 @@ wired into the CLI below. A few things are intentionally not there yet:
|
|
|
65
77
|
- **One sampling connector** (Netmiko/SSH). The CLI's `--connector` registry
|
|
66
78
|
is built to hold more without a redesign, but nothing else is wired in yet.
|
|
67
79
|
|
|
80
|
+
## Installation
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
pip install parseforge[anthropic]
|
|
84
|
+
```
|
|
85
|
+
`pip install parseforge` alone installs no AI-provider SDK at all — every
|
|
86
|
+
command that's pure local processing (`canonical`/`readable`/`recognizers`,
|
|
87
|
+
`integration`, `promotion`) works with nothing further. Anything that calls an
|
|
88
|
+
LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
|
|
89
|
+
extra for whichever provider it uses: `anthropic`, `openai`, or `deepseek`.
|
|
90
|
+
`--provider` defaults to `anthropic` wherever it isn't required, so that's the
|
|
91
|
+
one most setups need. `pip install parseforge[sampling]` adds Netmiko for live
|
|
92
|
+
device sampling; combine extras as needed, e.g.
|
|
93
|
+
`pip install parseforge[anthropic,openai,deepseek,sampling]`.
|
|
94
|
+
|
|
68
95
|
## Development
|
|
69
96
|
|
|
70
97
|
```
|
|
71
98
|
pip install -e ".[dev,sampling]"
|
|
72
99
|
pytest
|
|
73
100
|
```
|
|
101
|
+
`dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
|
|
102
|
+
all three providers — `anthropic`, `openai`, `deepseek` share just those two
|
|
103
|
+
packages — and never silently skip) — add `,anthropic`/`,openai`/`,deepseek`
|
|
104
|
+
explicitly only if installing outside of `dev`.
|
|
105
|
+
|
|
106
|
+
Linting/formatting/type-checking/docs run through tox instead of extras — see
|
|
107
|
+
`tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
|
|
108
|
+
tools in an isolated env. Cutting a release needs `pip install -e ".[release]"`
|
|
109
|
+
(`bump2version`, `build`) — see `scripts/release.ps1`.
|
|
74
110
|
|
|
75
111
|
## CLI
|
|
76
112
|
|
|
@@ -87,8 +123,8 @@ parseforge name --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.
|
|
|
87
123
|
```
|
|
88
124
|
Still needs an LLM provider on a cache miss — `--provider` defaults to `anthropic`,
|
|
89
125
|
and `--api-key` falls back to that provider's own env var (`ANTHROPIC_API_KEY`/
|
|
90
|
-
`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
|
|
91
|
-
LLM, so no key is needed at all in that case.
|
|
126
|
+
`OPENAI_API_KEY`/`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
|
|
127
|
+
never touches the LLM, so no key is needed at all in that case.
|
|
92
128
|
|
|
93
129
|
**`check`** — validate a connector or provider before spending time/tokens on a real
|
|
94
130
|
run. With neither `--env` nor explicit connection flags, prints what a connector needs
|
|
@@ -24,12 +24,36 @@ wired into the CLI below. A few things are intentionally not there yet:
|
|
|
24
24
|
- **One sampling connector** (Netmiko/SSH). The CLI's `--connector` registry
|
|
25
25
|
is built to hold more without a redesign, but nothing else is wired in yet.
|
|
26
26
|
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
pip install parseforge[anthropic]
|
|
31
|
+
```
|
|
32
|
+
`pip install parseforge` alone installs no AI-provider SDK at all — every
|
|
33
|
+
command that's pure local processing (`canonical`/`readable`/`recognizers`,
|
|
34
|
+
`integration`, `promotion`) works with nothing further. Anything that calls an
|
|
35
|
+
LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
|
|
36
|
+
extra for whichever provider it uses: `anthropic`, `openai`, or `deepseek`.
|
|
37
|
+
`--provider` defaults to `anthropic` wherever it isn't required, so that's the
|
|
38
|
+
one most setups need. `pip install parseforge[sampling]` adds Netmiko for live
|
|
39
|
+
device sampling; combine extras as needed, e.g.
|
|
40
|
+
`pip install parseforge[anthropic,openai,deepseek,sampling]`.
|
|
41
|
+
|
|
27
42
|
## Development
|
|
28
43
|
|
|
29
44
|
```
|
|
30
45
|
pip install -e ".[dev,sampling]"
|
|
31
46
|
pytest
|
|
32
47
|
```
|
|
48
|
+
`dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
|
|
49
|
+
all three providers — `anthropic`, `openai`, `deepseek` share just those two
|
|
50
|
+
packages — and never silently skip) — add `,anthropic`/`,openai`/`,deepseek`
|
|
51
|
+
explicitly only if installing outside of `dev`.
|
|
52
|
+
|
|
53
|
+
Linting/formatting/type-checking/docs run through tox instead of extras — see
|
|
54
|
+
`tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
|
|
55
|
+
tools in an isolated env. Cutting a release needs `pip install -e ".[release]"`
|
|
56
|
+
(`bump2version`, `build`) — see `scripts/release.ps1`.
|
|
33
57
|
|
|
34
58
|
## CLI
|
|
35
59
|
|
|
@@ -46,8 +70,8 @@ parseforge name --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.
|
|
|
46
70
|
```
|
|
47
71
|
Still needs an LLM provider on a cache miss — `--provider` defaults to `anthropic`,
|
|
48
72
|
and `--api-key` falls back to that provider's own env var (`ANTHROPIC_API_KEY`/
|
|
49
|
-
`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
|
|
50
|
-
LLM, so no key is needed at all in that case.
|
|
73
|
+
`OPENAI_API_KEY`/`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
|
|
74
|
+
never touches the LLM, so no key is needed at all in that case.
|
|
51
75
|
|
|
52
76
|
**`check`** — validate a connector or provider before spending time/tokens on a real
|
|
53
77
|
run. With neither `--env` nor explicit connection flags, prints what a connector needs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.5"
|
|
@@ -24,6 +24,7 @@ from parseforge.cli import config as cli_config
|
|
|
24
24
|
_BUILDERS: dict[str, type[naming.RegexBuilder]] = {
|
|
25
25
|
"anthropic": naming.AnthropicRegexBuilder,
|
|
26
26
|
"deepseek": naming.DeepSeekRegexBuilder,
|
|
27
|
+
"openai": naming.OpenAIRegexBuilder,
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
_CONNECTORS = ("netmiko",)
|
|
@@ -198,7 +199,8 @@ def main() -> None:
|
|
|
198
199
|
"--api-key",
|
|
199
200
|
default=None,
|
|
200
201
|
help="Provider API key. Defaults to that provider's own API key environment "
|
|
201
|
-
"variable (ANTHROPIC_API_KEY, DEEPSEEK_API_KEY); only needed
|
|
202
|
+
"variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY); only needed "
|
|
203
|
+
"on a cache miss.",
|
|
202
204
|
)
|
|
203
205
|
@click.option(
|
|
204
206
|
"--model",
|
|
@@ -266,7 +268,7 @@ def name_cmd(
|
|
|
266
268
|
"--provider",
|
|
267
269
|
required=True,
|
|
268
270
|
help="LLM provider for template generation (textfsm-ai's own registry, "
|
|
269
|
-
'e.g. "anthropic", "deepseek"). Naming uses its own separate '
|
|
271
|
+
'e.g. "anthropic", "openai", "deepseek"). Naming uses its own separate '
|
|
270
272
|
"--naming-provider, not this one.",
|
|
271
273
|
)
|
|
272
274
|
@click.option("--api-key", required=True, help="API key for the generation LLM call.")
|
|
@@ -78,7 +78,12 @@ def generate(
|
|
|
78
78
|
forwards to ``run_pipeline()`` for everything else (``endpoint``,
|
|
79
79
|
``region``, ``max_tries``, ...).
|
|
80
80
|
|
|
81
|
-
Never raises for a failed generation
|
|
81
|
+
Never raises for a failed *generation* (a bad/truncated LLM response) —
|
|
82
|
+
check ``.ready`` for that. Does raise ``ImportError`` if ``provider``'s
|
|
83
|
+
SDK isn't installed (textfsm-ai's own lazy provider registry, pointing
|
|
84
|
+
at the right ``pip install textfsm-ai[<provider>]`` extra) — a missing
|
|
85
|
+
package is an environment problem, not a generation outcome, and is
|
|
86
|
+
treated the same way here as in :mod:`parseforge.naming.providers`.
|
|
82
87
|
"""
|
|
83
88
|
result = run_pipeline(
|
|
84
89
|
sample, provider, api_key, model, mode="debug", as_json=True, **kwargs
|
|
@@ -8,7 +8,7 @@ from .llm import (
|
|
|
8
8
|
UnimplementedRegexBuilder,
|
|
9
9
|
build_prompt,
|
|
10
10
|
)
|
|
11
|
-
from .providers import AnthropicRegexBuilder, DeepSeekRegexBuilder
|
|
11
|
+
from .providers import AnthropicRegexBuilder, DeepSeekRegexBuilder, OpenAIRegexBuilder
|
|
12
12
|
from .resolver import NamingResolution, cli_name, resolve_cli_name
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
@@ -20,6 +20,7 @@ __all__ = [
|
|
|
20
20
|
"UnimplementedRegexBuilder",
|
|
21
21
|
"AnthropicRegexBuilder",
|
|
22
22
|
"DeepSeekRegexBuilder",
|
|
23
|
+
"OpenAIRegexBuilder",
|
|
23
24
|
"LLMCLIResponse",
|
|
24
25
|
"TokenUsage",
|
|
25
26
|
"build_prompt",
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from anthropic import Anthropic, AnthropicError
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
9
7
|
|
|
10
8
|
from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
|
|
11
9
|
from .cost import estimate_cost
|
|
@@ -13,6 +11,9 @@ from .errors import format_llm_error_reason, is_retryable
|
|
|
13
11
|
from .models import default_model
|
|
14
12
|
from .text import extract_pattern
|
|
15
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import anthropic as anthropic_sdk
|
|
16
|
+
|
|
16
17
|
DEFAULT_MODEL = default_model("anthropic")
|
|
17
18
|
|
|
18
19
|
# Headroom in case a future/opt-in extended-thinking model burns part of
|
|
@@ -21,6 +22,21 @@ DEFAULT_MODEL = default_model("anthropic")
|
|
|
21
22
|
_DEFAULT_MAX_TOKENS = 1024
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
def _import_anthropic() -> Any:
|
|
26
|
+
"""Deferred import — anthropic is an optional extra (parseforge[anthropic]);
|
|
27
|
+
nothing else in this module (or a cache-hit lookup, which never reaches
|
|
28
|
+
build_pattern at all — see resolver.cli_name) should require it to be
|
|
29
|
+
installed."""
|
|
30
|
+
try:
|
|
31
|
+
import anthropic
|
|
32
|
+
except ImportError as exc:
|
|
33
|
+
raise ImportError(
|
|
34
|
+
"the anthropic package is required to use AnthropicRegexBuilder — "
|
|
35
|
+
"install it via `pip install parseforge[anthropic]`"
|
|
36
|
+
) from exc
|
|
37
|
+
return anthropic
|
|
38
|
+
|
|
39
|
+
|
|
24
40
|
class AnthropicRegexBuilder:
|
|
25
41
|
"""Builds a cli-name regex pattern by prompting a Claude model.
|
|
26
42
|
|
|
@@ -38,18 +54,22 @@ class AnthropicRegexBuilder:
|
|
|
38
54
|
def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
|
|
39
55
|
self.model = model
|
|
40
56
|
self._api_key = api_key
|
|
41
|
-
self._client: Anthropic | None = None
|
|
57
|
+
self._client: anthropic_sdk.Anthropic | None = None
|
|
42
58
|
|
|
43
|
-
def _get_client(self) -> Anthropic:
|
|
59
|
+
def _get_client(self) -> anthropic_sdk.Anthropic:
|
|
44
60
|
if self._client is None:
|
|
61
|
+
anthropic = _import_anthropic()
|
|
45
62
|
self._client = (
|
|
46
|
-
Anthropic(api_key=self._api_key)
|
|
63
|
+
anthropic.Anthropic(api_key=self._api_key)
|
|
64
|
+
if self._api_key
|
|
65
|
+
else anthropic.Anthropic()
|
|
47
66
|
)
|
|
48
67
|
return self._client
|
|
49
68
|
|
|
50
69
|
def build_pattern(
|
|
51
70
|
self, command: str, context: CliContext, **kwargs: Any
|
|
52
71
|
) -> LLMCLIResponse:
|
|
72
|
+
anthropic = _import_anthropic()
|
|
53
73
|
prompt = build_prompt(command, context)
|
|
54
74
|
max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
|
|
55
75
|
|
|
@@ -61,7 +81,7 @@ class AnthropicRegexBuilder:
|
|
|
61
81
|
messages=[{"role": "user", "content": prompt}],
|
|
62
82
|
**kwargs,
|
|
63
83
|
)
|
|
64
|
-
except AnthropicError as exc:
|
|
84
|
+
except anthropic.AnthropicError as exc:
|
|
65
85
|
if not is_retryable(exc):
|
|
66
86
|
# Same request would fail the same way again — stop rather
|
|
67
87
|
# than let a caller burn another attempt on it.
|
|
@@ -9,9 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
11
|
import time
|
|
12
|
-
from typing import Any
|
|
13
|
-
|
|
14
|
-
from openai import OpenAI, OpenAIError
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
15
13
|
|
|
16
14
|
from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
|
|
17
15
|
from .cost import estimate_cost
|
|
@@ -19,6 +17,9 @@ from .errors import format_llm_error_reason, is_retryable
|
|
|
19
17
|
from .models import default_model
|
|
20
18
|
from .text import extract_pattern
|
|
21
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import openai as openai_sdk
|
|
22
|
+
|
|
22
23
|
DEFAULT_MODEL = default_model("deepseek")
|
|
23
24
|
|
|
24
25
|
_BASE_URL = "https://api.deepseek.com"
|
|
@@ -34,6 +35,21 @@ _DEFAULT_MAX_TOKENS = 1024
|
|
|
34
35
|
_THINKING_DISABLED = {"thinking": {"type": "disabled"}}
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def _import_openai() -> Any:
|
|
39
|
+
"""Deferred import — openai is an optional extra (parseforge[deepseek]);
|
|
40
|
+
nothing else in this module (or a cache-hit lookup, which never reaches
|
|
41
|
+
build_pattern at all — see resolver.cli_name) should require it to be
|
|
42
|
+
installed."""
|
|
43
|
+
try:
|
|
44
|
+
import openai
|
|
45
|
+
except ImportError as exc:
|
|
46
|
+
raise ImportError(
|
|
47
|
+
"the openai package is required to use DeepSeekRegexBuilder — "
|
|
48
|
+
"install it via `pip install parseforge[deepseek]`"
|
|
49
|
+
) from exc
|
|
50
|
+
return openai
|
|
51
|
+
|
|
52
|
+
|
|
37
53
|
class DeepSeekRegexBuilder:
|
|
38
54
|
"""Builds a cli-name regex pattern by prompting a DeepSeek model.
|
|
39
55
|
|
|
@@ -53,21 +69,23 @@ class DeepSeekRegexBuilder:
|
|
|
53
69
|
def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
|
|
54
70
|
self.model = model
|
|
55
71
|
self._api_key = api_key
|
|
56
|
-
self._client: OpenAI | None = None
|
|
72
|
+
self._client: openai_sdk.OpenAI | None = None
|
|
57
73
|
|
|
58
|
-
def _get_client(self) -> OpenAI:
|
|
74
|
+
def _get_client(self) -> openai_sdk.OpenAI:
|
|
59
75
|
if self._client is None:
|
|
76
|
+
openai = _import_openai()
|
|
60
77
|
api_key = self._api_key or os.environ.get("DEEPSEEK_API_KEY")
|
|
61
78
|
if not api_key:
|
|
62
79
|
raise RuntimeError(
|
|
63
80
|
"no DeepSeek API key — pass api_key or set DEEPSEEK_API_KEY"
|
|
64
81
|
)
|
|
65
|
-
self._client = OpenAI(api_key=api_key, base_url=_BASE_URL)
|
|
82
|
+
self._client = openai.OpenAI(api_key=api_key, base_url=_BASE_URL)
|
|
66
83
|
return self._client
|
|
67
84
|
|
|
68
85
|
def build_pattern(
|
|
69
86
|
self, command: str, context: CliContext, **kwargs: Any
|
|
70
87
|
) -> LLMCLIResponse:
|
|
88
|
+
openai = _import_openai()
|
|
71
89
|
prompt = build_prompt(command, context)
|
|
72
90
|
# Callers can override either of these per-call (e.g. re-enable
|
|
73
91
|
# thinking mode, raise max_tokens further); anything else in
|
|
@@ -84,7 +102,7 @@ class DeepSeekRegexBuilder:
|
|
|
84
102
|
extra_body=extra_body,
|
|
85
103
|
**kwargs,
|
|
86
104
|
)
|
|
87
|
-
except OpenAIError as exc:
|
|
105
|
+
except openai.OpenAIError as exc:
|
|
88
106
|
if not is_retryable(exc):
|
|
89
107
|
# Same request would fail the same way again — stop rather
|
|
90
108
|
# than let a caller burn another attempt on it.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""OpenAI-backed RegexBuilder implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
|
|
9
|
+
from .cost import estimate_cost
|
|
10
|
+
from .errors import format_llm_error_reason, is_retryable
|
|
11
|
+
from .models import default_model
|
|
12
|
+
from .text import extract_pattern
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import openai as openai_sdk
|
|
16
|
+
|
|
17
|
+
DEFAULT_MODEL = default_model("openai")
|
|
18
|
+
|
|
19
|
+
_DEFAULT_MAX_TOKENS = 1024
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _import_openai() -> Any:
|
|
23
|
+
"""Deferred import — openai is an optional extra (parseforge[openai]);
|
|
24
|
+
nothing else in this module (or a cache-hit lookup, which never reaches
|
|
25
|
+
build_pattern at all — see resolver.cli_name) should require it to be
|
|
26
|
+
installed."""
|
|
27
|
+
try:
|
|
28
|
+
import openai
|
|
29
|
+
except ImportError as exc:
|
|
30
|
+
raise ImportError(
|
|
31
|
+
"the openai package is required to use OpenAIRegexBuilder — "
|
|
32
|
+
"install it via `pip install parseforge[openai]`"
|
|
33
|
+
) from exc
|
|
34
|
+
return openai
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OpenAIRegexBuilder:
|
|
38
|
+
"""Builds a cli-name regex pattern by prompting an OpenAI model.
|
|
39
|
+
|
|
40
|
+
The client is constructed lazily, on the first actual call — not in
|
|
41
|
+
``__init__`` — so this can be used as a default RegexBuilder without
|
|
42
|
+
requiring ``OPENAI_API_KEY`` to be set for cache-hit lookups, which
|
|
43
|
+
never reach the LLM at all (see resolver.cli_name).
|
|
44
|
+
|
|
45
|
+
API key resolves from the ``api_key`` argument if given, otherwise the
|
|
46
|
+
``OPENAI_API_KEY`` environment variable (the SDK's own default).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
provider = "openai"
|
|
50
|
+
|
|
51
|
+
def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
|
|
52
|
+
self.model = model
|
|
53
|
+
self._api_key = api_key
|
|
54
|
+
self._client: openai_sdk.OpenAI | None = None
|
|
55
|
+
|
|
56
|
+
def _get_client(self) -> openai_sdk.OpenAI:
|
|
57
|
+
if self._client is None:
|
|
58
|
+
openai = _import_openai()
|
|
59
|
+
self._client = (
|
|
60
|
+
openai.OpenAI(api_key=self._api_key)
|
|
61
|
+
if self._api_key
|
|
62
|
+
else openai.OpenAI()
|
|
63
|
+
)
|
|
64
|
+
return self._client
|
|
65
|
+
|
|
66
|
+
def build_pattern(
|
|
67
|
+
self, command: str, context: CliContext, **kwargs: Any
|
|
68
|
+
) -> LLMCLIResponse:
|
|
69
|
+
openai = _import_openai()
|
|
70
|
+
prompt = build_prompt(command, context)
|
|
71
|
+
max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
|
|
72
|
+
|
|
73
|
+
start = time.monotonic()
|
|
74
|
+
try:
|
|
75
|
+
response = self._get_client().chat.completions.create(
|
|
76
|
+
model=self.model,
|
|
77
|
+
max_tokens=max_tokens,
|
|
78
|
+
messages=[{"role": "user", "content": prompt}],
|
|
79
|
+
**kwargs,
|
|
80
|
+
)
|
|
81
|
+
except openai.OpenAIError as exc:
|
|
82
|
+
if not is_retryable(exc):
|
|
83
|
+
# Same request would fail the same way again — stop rather
|
|
84
|
+
# than let a caller burn another attempt on it.
|
|
85
|
+
raise
|
|
86
|
+
return LLMCLIResponse(
|
|
87
|
+
content="",
|
|
88
|
+
raw=exc,
|
|
89
|
+
usage=TokenUsage(
|
|
90
|
+
input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
|
|
91
|
+
),
|
|
92
|
+
duration_ms=(time.monotonic() - start) * 1000,
|
|
93
|
+
reason=format_llm_error_reason(exc),
|
|
94
|
+
ready=False,
|
|
95
|
+
)
|
|
96
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
97
|
+
|
|
98
|
+
choice = response.choices[0]
|
|
99
|
+
return LLMCLIResponse(
|
|
100
|
+
content=extract_pattern(choice.message.content or ""),
|
|
101
|
+
raw=response,
|
|
102
|
+
usage=TokenUsage(
|
|
103
|
+
input_tokens=response.usage.prompt_tokens,
|
|
104
|
+
output_tokens=response.usage.completion_tokens,
|
|
105
|
+
total_tokens=response.usage.total_tokens,
|
|
106
|
+
estimated_cost=estimate_cost(
|
|
107
|
+
input_tokens=response.usage.prompt_tokens,
|
|
108
|
+
output_tokens=response.usage.completion_tokens,
|
|
109
|
+
total_tokens=response.usage.total_tokens,
|
|
110
|
+
provider=self.provider,
|
|
111
|
+
model=self.model,
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
duration_ms=duration_ms,
|
|
115
|
+
reason=choice.finish_reason or "",
|
|
116
|
+
ready=choice.finish_reason == "stop",
|
|
117
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: parseforge
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
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>
|
|
@@ -26,9 +26,16 @@ License-File: LICENSE
|
|
|
26
26
|
Requires-Dist: click>=8.1
|
|
27
27
|
Requires-Dist: PyYAML>=6.0
|
|
28
28
|
Requires-Dist: textfsm>=1.1.0
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
|
|
31
|
-
Requires-Dist:
|
|
29
|
+
Requires-Dist: textfsm-ai>=0.6.1
|
|
30
|
+
Provides-Extra: anthropic
|
|
31
|
+
Requires-Dist: anthropic>=0.40.0; extra == "anthropic"
|
|
32
|
+
Requires-Dist: textfsm-ai[anthropic]>=0.6.1; extra == "anthropic"
|
|
33
|
+
Provides-Extra: deepseek
|
|
34
|
+
Requires-Dist: openai>=1.0.0; extra == "deepseek"
|
|
35
|
+
Requires-Dist: textfsm-ai[deepseek]>=0.6.1; extra == "deepseek"
|
|
36
|
+
Provides-Extra: openai
|
|
37
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
38
|
+
Requires-Dist: textfsm-ai[openai]>=0.6.1; extra == "openai"
|
|
32
39
|
Provides-Extra: sampling
|
|
33
40
|
Requires-Dist: netmiko>=4.0; extra == "sampling"
|
|
34
41
|
Provides-Extra: dev
|
|
@@ -37,6 +44,11 @@ Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
|
37
44
|
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
38
45
|
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
39
46
|
Requires-Dist: mypy>=1.10.0; extra == "dev"
|
|
47
|
+
Requires-Dist: anthropic>=0.40.0; extra == "dev"
|
|
48
|
+
Requires-Dist: openai>=1.0.0; extra == "dev"
|
|
49
|
+
Provides-Extra: release
|
|
50
|
+
Requires-Dist: bump2version; extra == "release"
|
|
51
|
+
Requires-Dist: build; extra == "release"
|
|
40
52
|
Dynamic: license-file
|
|
41
53
|
|
|
42
54
|
# ParseForge
|
|
@@ -65,12 +77,36 @@ wired into the CLI below. A few things are intentionally not there yet:
|
|
|
65
77
|
- **One sampling connector** (Netmiko/SSH). The CLI's `--connector` registry
|
|
66
78
|
is built to hold more without a redesign, but nothing else is wired in yet.
|
|
67
79
|
|
|
80
|
+
## Installation
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
pip install parseforge[anthropic]
|
|
84
|
+
```
|
|
85
|
+
`pip install parseforge` alone installs no AI-provider SDK at all — every
|
|
86
|
+
command that's pure local processing (`canonical`/`readable`/`recognizers`,
|
|
87
|
+
`integration`, `promotion`) works with nothing further. Anything that calls an
|
|
88
|
+
LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
|
|
89
|
+
extra for whichever provider it uses: `anthropic`, `openai`, or `deepseek`.
|
|
90
|
+
`--provider` defaults to `anthropic` wherever it isn't required, so that's the
|
|
91
|
+
one most setups need. `pip install parseforge[sampling]` adds Netmiko for live
|
|
92
|
+
device sampling; combine extras as needed, e.g.
|
|
93
|
+
`pip install parseforge[anthropic,openai,deepseek,sampling]`.
|
|
94
|
+
|
|
68
95
|
## Development
|
|
69
96
|
|
|
70
97
|
```
|
|
71
98
|
pip install -e ".[dev,sampling]"
|
|
72
99
|
pytest
|
|
73
100
|
```
|
|
101
|
+
`dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
|
|
102
|
+
all three providers — `anthropic`, `openai`, `deepseek` share just those two
|
|
103
|
+
packages — and never silently skip) — add `,anthropic`/`,openai`/`,deepseek`
|
|
104
|
+
explicitly only if installing outside of `dev`.
|
|
105
|
+
|
|
106
|
+
Linting/formatting/type-checking/docs run through tox instead of extras — see
|
|
107
|
+
`tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
|
|
108
|
+
tools in an isolated env. Cutting a release needs `pip install -e ".[release]"`
|
|
109
|
+
(`bump2version`, `build`) — see `scripts/release.ps1`.
|
|
74
110
|
|
|
75
111
|
## CLI
|
|
76
112
|
|
|
@@ -87,8 +123,8 @@ parseforge name --vendor cisco --family catalyst9200 --os ios-xe --version 17.9.
|
|
|
87
123
|
```
|
|
88
124
|
Still needs an LLM provider on a cache miss — `--provider` defaults to `anthropic`,
|
|
89
125
|
and `--api-key` falls back to that provider's own env var (`ANTHROPIC_API_KEY`/
|
|
90
|
-
`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
|
|
91
|
-
LLM, so no key is needed at all in that case.
|
|
126
|
+
`OPENAI_API_KEY`/`DEEPSEEK_API_KEY`). A cache hit (a command already seen before)
|
|
127
|
+
never touches the LLM, so no key is needed at all in that case.
|
|
92
128
|
|
|
93
129
|
**`check`** — validate a connector or provider before spending time/tokens on a real
|
|
94
130
|
run. With neither `--env` nor explicit connection flags, prints what a connector needs
|
|
@@ -34,6 +34,7 @@ parseforge/naming/providers/deepseek.py
|
|
|
34
34
|
parseforge/naming/providers/errors.py
|
|
35
35
|
parseforge/naming/providers/models.py
|
|
36
36
|
parseforge/naming/providers/models.yaml
|
|
37
|
+
parseforge/naming/providers/openai.py
|
|
37
38
|
parseforge/naming/providers/text.py
|
|
38
39
|
parseforge/sampling/__init__.py
|
|
39
40
|
parseforge/sampling/core.py
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
click>=8.1
|
|
2
|
+
PyYAML>=6.0
|
|
3
|
+
textfsm>=1.1.0
|
|
4
|
+
textfsm-ai>=0.6.1
|
|
5
|
+
|
|
6
|
+
[anthropic]
|
|
7
|
+
anthropic>=0.40.0
|
|
8
|
+
textfsm-ai[anthropic]>=0.6.1
|
|
9
|
+
|
|
10
|
+
[deepseek]
|
|
11
|
+
openai>=1.0.0
|
|
12
|
+
textfsm-ai[deepseek]>=0.6.1
|
|
13
|
+
|
|
14
|
+
[dev]
|
|
15
|
+
pytest>=8.0.0
|
|
16
|
+
pytest-cov>=5.0.0
|
|
17
|
+
ruff>=0.4.0
|
|
18
|
+
black>=24.0.0
|
|
19
|
+
mypy>=1.10.0
|
|
20
|
+
anthropic>=0.40.0
|
|
21
|
+
openai>=1.0.0
|
|
22
|
+
|
|
23
|
+
[openai]
|
|
24
|
+
openai>=1.0.0
|
|
25
|
+
textfsm-ai[openai]>=0.6.1
|
|
26
|
+
|
|
27
|
+
[release]
|
|
28
|
+
bump2version
|
|
29
|
+
build
|
|
30
|
+
|
|
31
|
+
[sampling]
|
|
32
|
+
netmiko>=4.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "parseforge"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.5"
|
|
8
8
|
description = "LLM-driven pipeline that forges, validates, and promotes TextFSM templates from network CLI output"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -22,9 +22,7 @@ dependencies = [
|
|
|
22
22
|
"click>=8.1",
|
|
23
23
|
"PyYAML>=6.0",
|
|
24
24
|
"textfsm>=1.1.0",
|
|
25
|
-
"
|
|
26
|
-
"openai>=1.0.0",
|
|
27
|
-
"textfsm-ai>=0.5.1",
|
|
25
|
+
"textfsm-ai>=0.6.1",
|
|
28
26
|
]
|
|
29
27
|
|
|
30
28
|
classifiers = [
|
|
@@ -59,6 +57,14 @@ keywords = [
|
|
|
59
57
|
]
|
|
60
58
|
|
|
61
59
|
[project.optional-dependencies]
|
|
60
|
+
# textfsm-ai v0.6.0+ made every provider SDK an optional extra (lazy
|
|
61
|
+
# import; a clear ImportError only when that provider is actually used).
|
|
62
|
+
# parseforge mirrors that per-provider: `pip install parseforge` alone
|
|
63
|
+
# now installs no AI-provider SDK at all -- add the extra(s) for the
|
|
64
|
+
# provider(s) you actually use.
|
|
65
|
+
anthropic = ["anthropic>=0.40.0", "textfsm-ai[anthropic]>=0.6.1"]
|
|
66
|
+
deepseek = ["openai>=1.0.0", "textfsm-ai[deepseek]>=0.6.1"]
|
|
67
|
+
openai = ["openai>=1.0.0", "textfsm-ai[openai]>=0.6.1"]
|
|
62
68
|
sampling = ["netmiko>=4.0"]
|
|
63
69
|
dev = [
|
|
64
70
|
"pytest>=8.0.0",
|
|
@@ -66,6 +72,19 @@ dev = [
|
|
|
66
72
|
"ruff>=0.4.0",
|
|
67
73
|
"black>=24.0.0",
|
|
68
74
|
"mypy>=1.10.0",
|
|
75
|
+
# anthropic/openai are optional at runtime (see the extras above), but
|
|
76
|
+
# both providers are exercised by the CLI/tests, which should never
|
|
77
|
+
# silently skip -- so dev always pulls both in.
|
|
78
|
+
"anthropic>=0.40.0",
|
|
79
|
+
"openai>=1.0.0",
|
|
80
|
+
]
|
|
81
|
+
# Tooling for scripts/release.ps1 and CI's own package-build step.
|
|
82
|
+
# bump2version isn't otherwise installed anywhere -- release.ps1 assumes
|
|
83
|
+
# it's already on PATH. twine is deliberately not here: the actual PyPI
|
|
84
|
+
# publish goes through pypa/gh-action-pypi-publish, not `twine upload`.
|
|
85
|
+
release = [
|
|
86
|
+
"bump2version",
|
|
87
|
+
"build",
|
|
69
88
|
]
|
|
70
89
|
|
|
71
90
|
[project.scripts]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|