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.
Files changed (47) hide show
  1. {parseforge-0.2.3/parseforge.egg-info → parseforge-0.2.5}/PKG-INFO +42 -6
  2. {parseforge-0.2.3 → parseforge-0.2.5}/README.md +26 -2
  3. parseforge-0.2.5/parseforge/__init__.py +1 -0
  4. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/cli/main.py +4 -2
  5. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/generation.py +6 -1
  6. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/__init__.py +2 -1
  7. parseforge-0.2.5/parseforge/naming/providers/__init__.py +5 -0
  8. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/anthropic.py +27 -7
  9. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/deepseek.py +25 -7
  10. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/models.yaml +9 -0
  11. parseforge-0.2.5/parseforge/naming/providers/openai.py +117 -0
  12. {parseforge-0.2.3 → parseforge-0.2.5/parseforge.egg-info}/PKG-INFO +42 -6
  13. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge.egg-info/SOURCES.txt +1 -0
  14. parseforge-0.2.5/parseforge.egg-info/requires.txt +32 -0
  15. {parseforge-0.2.3 → parseforge-0.2.5}/pyproject.toml +23 -4
  16. parseforge-0.2.3/parseforge/__init__.py +0 -1
  17. parseforge-0.2.3/parseforge/naming/providers/__init__.py +0 -4
  18. parseforge-0.2.3/parseforge.egg-info/requires.txt +0 -16
  19. {parseforge-0.2.3 → parseforge-0.2.5}/LICENSE +0 -0
  20. {parseforge-0.2.3 → parseforge-0.2.5}/MANIFEST.in +0 -0
  21. {parseforge-0.2.3 → parseforge-0.2.5}/SPEC.md +0 -0
  22. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/cli/__init__.py +0 -0
  23. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/cli/config.py +0 -0
  24. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/drift.py +0 -0
  25. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/integration.py +0 -0
  26. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/assemble.py +0 -0
  27. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/cache.py +0 -0
  28. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/llm.py +0 -0
  29. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/prompts.py +0 -0
  30. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/prompts.yaml +0 -0
  31. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/cost.py +0 -0
  32. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/errors.py +0 -0
  33. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/models.py +0 -0
  34. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/providers/text.py +0 -0
  35. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/naming/resolver.py +0 -0
  36. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/paths.py +0 -0
  37. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/pipeline.py +0 -0
  38. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/promotion.py +0 -0
  39. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/sampling/__init__.py +0 -0
  40. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/sampling/backends/__init__.py +0 -0
  41. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/sampling/backends/netmiko.py +0 -0
  42. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/sampling/core.py +0 -0
  43. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge/validation.py +0 -0
  44. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge.egg-info/dependency_links.txt +0 -0
  45. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge.egg-info/entry_points.txt +0 -0
  46. {parseforge-0.2.3 → parseforge-0.2.5}/parseforge.egg-info/top_level.txt +0 -0
  47. {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
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: anthropic>=0.40.0
30
- Requires-Dist: openai>=1.0.0
31
- Requires-Dist: textfsm-ai>=0.5.1
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) never touches the
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) never touches the
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 on a cache miss.",
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 check ``.ready``.
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",
@@ -0,0 +1,5 @@
1
+ from .anthropic import AnthropicRegexBuilder
2
+ from .deepseek import DeepSeekRegexBuilder
3
+ from .openai import OpenAIRegexBuilder
4
+
5
+ __all__ = ["AnthropicRegexBuilder", "DeepSeekRegexBuilder", "OpenAIRegexBuilder"]
@@ -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) if self._api_key else Anthropic()
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.
@@ -22,3 +22,12 @@ deepseek:
22
22
  - deepseek-v4-flash
23
23
  - deepseek-v4-pro
24
24
  deprecated: []
25
+
26
+ openai:
27
+ default: gpt-5.4-mini
28
+ supported:
29
+ - gpt-5.4-mini
30
+ - gpt-5.4
31
+ - gpt-5.4-nano
32
+ - gpt-5.5
33
+ deprecated: []
@@ -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
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: anthropic>=0.40.0
30
- Requires-Dist: openai>=1.0.0
31
- Requires-Dist: textfsm-ai>=0.5.1
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) never touches the
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.3"
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
- "anthropic>=0.40.0",
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"
@@ -1,4 +0,0 @@
1
- from .anthropic import AnthropicRegexBuilder
2
- from .deepseek import DeepSeekRegexBuilder
3
-
4
- __all__ = ["AnthropicRegexBuilder", "DeepSeekRegexBuilder"]
@@ -1,16 +0,0 @@
1
- click>=8.1
2
- PyYAML>=6.0
3
- textfsm>=1.1.0
4
- anthropic>=0.40.0
5
- openai>=1.0.0
6
- textfsm-ai>=0.5.1
7
-
8
- [dev]
9
- pytest>=8.0.0
10
- pytest-cov>=5.0.0
11
- ruff>=0.4.0
12
- black>=24.0.0
13
- mypy>=1.10.0
14
-
15
- [sampling]
16
- netmiko>=4.0
File without changes
File without changes
File without changes
File without changes