parseforge 0.2.6__tar.gz → 0.2.8__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 (55) hide show
  1. {parseforge-0.2.6/parseforge.egg-info → parseforge-0.2.8}/PKG-INFO +21 -11
  2. {parseforge-0.2.6 → parseforge-0.2.8}/README.md +12 -10
  3. parseforge-0.2.8/parseforge/__init__.py +1 -0
  4. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/cli/main.py +7 -4
  5. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/__init__.py +4 -0
  6. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/__init__.py +4 -0
  7. parseforge-0.2.8/parseforge/naming/providers/cohere.py +173 -0
  8. parseforge-0.2.8/parseforge/naming/providers/mistral.py +163 -0
  9. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/models.yaml +19 -0
  10. {parseforge-0.2.6 → parseforge-0.2.8/parseforge.egg-info}/PKG-INFO +21 -11
  11. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge.egg-info/SOURCES.txt +2 -0
  12. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge.egg-info/requires.txt +10 -0
  13. {parseforge-0.2.6 → parseforge-0.2.8}/pyproject.toml +14 -4
  14. parseforge-0.2.6/parseforge/__init__.py +0 -1
  15. {parseforge-0.2.6 → parseforge-0.2.8}/LICENSE +0 -0
  16. {parseforge-0.2.6 → parseforge-0.2.8}/MANIFEST.in +0 -0
  17. {parseforge-0.2.6 → parseforge-0.2.8}/SPEC.md +0 -0
  18. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/cli/__init__.py +0 -0
  19. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/cli/config.py +0 -0
  20. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/drift.py +0 -0
  21. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/generation.py +0 -0
  22. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/integration.py +0 -0
  23. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/assemble.py +0 -0
  24. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/cache.py +0 -0
  25. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/llm.py +0 -0
  26. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/prompts.py +0 -0
  27. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/prompts.yaml +0 -0
  28. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/anthropic.py +0 -0
  29. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/cerebras.py +0 -0
  30. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/cost.py +0 -0
  31. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/deepseek.py +0 -0
  32. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/errors.py +0 -0
  33. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/fireworks.py +0 -0
  34. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/groq.py +0 -0
  35. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/models.py +0 -0
  36. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/moonshot.py +0 -0
  37. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/openai.py +0 -0
  38. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/openrouter.py +0 -0
  39. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/perplexity.py +0 -0
  40. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/text.py +0 -0
  41. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/together.py +0 -0
  42. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/xai.py +0 -0
  43. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/resolver.py +0 -0
  44. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/paths.py +0 -0
  45. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/pipeline.py +0 -0
  46. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/promotion.py +0 -0
  47. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/sampling/__init__.py +0 -0
  48. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/sampling/backends/__init__.py +0 -0
  49. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/sampling/backends/netmiko.py +0 -0
  50. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/sampling/core.py +0 -0
  51. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/validation.py +0 -0
  52. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge.egg-info/dependency_links.txt +0 -0
  53. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge.egg-info/entry_points.txt +0 -0
  54. {parseforge-0.2.6 → parseforge-0.2.8}/parseforge.egg-info/top_level.txt +0 -0
  55. {parseforge-0.2.6 → parseforge-0.2.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parseforge
3
- Version: 0.2.6
3
+ Version: 0.2.8
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>
@@ -60,6 +60,12 @@ Requires-Dist: textfsm-ai[moonshot]>=0.6.1; extra == "moonshot"
60
60
  Provides-Extra: cerebras
61
61
  Requires-Dist: openai>=1.0.0; extra == "cerebras"
62
62
  Requires-Dist: textfsm-ai[cerebras]>=0.6.1; extra == "cerebras"
63
+ Provides-Extra: mistral
64
+ Requires-Dist: mistralai==1.10.0; extra == "mistral"
65
+ Requires-Dist: textfsm-ai[mistral]>=0.6.1; extra == "mistral"
66
+ Provides-Extra: cohere
67
+ Requires-Dist: cohere==5.21.1; extra == "cohere"
68
+ Requires-Dist: textfsm-ai[cohere]>=0.6.1; extra == "cohere"
63
69
  Provides-Extra: sampling
64
70
  Requires-Dist: netmiko>=4.0; extra == "sampling"
65
71
  Provides-Extra: dev
@@ -70,6 +76,8 @@ Requires-Dist: black>=24.0.0; extra == "dev"
70
76
  Requires-Dist: mypy>=1.10.0; extra == "dev"
71
77
  Requires-Dist: anthropic>=0.40.0; extra == "dev"
72
78
  Requires-Dist: openai>=1.0.0; extra == "dev"
79
+ Requires-Dist: mistralai==1.10.0; extra == "dev"
80
+ Requires-Dist: cohere==5.21.1; extra == "dev"
73
81
  Provides-Extra: release
74
82
  Requires-Dist: bump2version; extra == "release"
75
83
  Requires-Dist: build; extra == "release"
@@ -112,11 +120,11 @@ command that's pure local processing (`canonical`/`readable`/`recognizers`,
112
120
  LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
113
121
  extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
114
122
  `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]`.
123
+ `moonshot`, `cerebras`, `mistral`, or `cohere`. `--provider` defaults to
124
+ `anthropic` wherever it isn't required, so that's the one most setups need.
125
+ `pip install parseforge[sampling]` adds Netmiko for live device sampling;
126
+ combine extras as needed, e.g.
127
+ `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,mistral,cohere,sampling]`.
120
128
 
121
129
  ## Development
122
130
 
@@ -124,11 +132,13 @@ as needed, e.g.
124
132
  pip install -e ".[dev,sampling]"
125
133
  pytest
126
134
  ```
127
- `dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
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`.
135
+ `dev` already includes the `anthropic`, `openai`, `mistralai`, and `cohere`
136
+ SDKs (tests exercise all thirteen providers — `anthropic`, `openai`,
137
+ `deepseek`, `groq`, `xai`, `together`, `fireworks`, `perplexity`,
138
+ `openrouter`, `moonshot`, `cerebras` share just the first two packages, and
139
+ `mistral`/`cohere` each need their own native SDK and never silently
140
+ skip) — add the specific `,<provider>` extra explicitly only if installing
141
+ outside of `dev`.
132
142
 
133
143
  Linting/formatting/type-checking/docs run through tox instead of extras — see
134
144
  `tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
@@ -35,11 +35,11 @@ command that's pure local processing (`canonical`/`readable`/`recognizers`,
35
35
  LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
36
36
  extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
37
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]`.
38
+ `moonshot`, `cerebras`, `mistral`, or `cohere`. `--provider` defaults to
39
+ `anthropic` wherever it isn't required, so that's the one most setups need.
40
+ `pip install parseforge[sampling]` adds Netmiko for live device sampling;
41
+ combine extras as needed, e.g.
42
+ `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,mistral,cohere,sampling]`.
43
43
 
44
44
  ## Development
45
45
 
@@ -47,11 +47,13 @@ as needed, e.g.
47
47
  pip install -e ".[dev,sampling]"
48
48
  pytest
49
49
  ```
50
- `dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
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`.
50
+ `dev` already includes the `anthropic`, `openai`, `mistralai`, and `cohere`
51
+ SDKs (tests exercise all thirteen providers — `anthropic`, `openai`,
52
+ `deepseek`, `groq`, `xai`, `together`, `fireworks`, `perplexity`,
53
+ `openrouter`, `moonshot`, `cerebras` share just the first two packages, and
54
+ `mistral`/`cohere` each need their own native SDK and never silently
55
+ skip) — add the specific `,<provider>` extra explicitly only if installing
56
+ outside of `dev`.
55
57
 
56
58
  Linting/formatting/type-checking/docs run through tox instead of extras — see
57
59
  `tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
@@ -0,0 +1 @@
1
+ __version__ = "0.2.8"
@@ -24,9 +24,11 @@ from parseforge.cli import config as cli_config
24
24
  _BUILDERS: dict[str, type[naming.RegexBuilder]] = {
25
25
  "anthropic": naming.AnthropicRegexBuilder,
26
26
  "cerebras": naming.CerebrasRegexBuilder,
27
+ "cohere": naming.CohereRegexBuilder,
27
28
  "deepseek": naming.DeepSeekRegexBuilder,
28
29
  "fireworks": naming.FireworksRegexBuilder,
29
30
  "groq": naming.GroqRegexBuilder,
31
+ "mistral": naming.MistralRegexBuilder,
30
32
  "moonshot": naming.MoonshotRegexBuilder,
31
33
  "openai": naming.OpenAIRegexBuilder,
32
34
  "openrouter": naming.OpenRouterRegexBuilder,
@@ -209,8 +211,8 @@ def main() -> None:
209
211
  help="Provider API key. Defaults to that provider's own API key environment "
210
212
  "variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY, GROQ_API_KEY, "
211
213
  "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.",
214
+ "OPENROUTER_API_KEY, MOONSHOT_API_KEY, CEREBRAS_API_KEY, MISTRAL_API_KEY, "
215
+ "COHERE_API_KEY); only needed on a cache miss.",
214
216
  )
215
217
  @click.option(
216
218
  "--model",
@@ -279,8 +281,9 @@ def name_cmd(
279
281
  required=True,
280
282
  help="LLM provider for template generation (textfsm-ai's own registry, "
281
283
  '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.",
284
+ '"fireworks", "perplexity", "openrouter", "moonshot", "cerebras", '
285
+ '"mistral", "cohere"). Naming uses its own separate --naming-provider, '
286
+ "not this one.",
284
287
  )
285
288
  @click.option("--api-key", required=True, help="API key for the generation LLM call.")
286
289
  @click.option("--model", required=True, help="Model for the generation LLM call.")
@@ -11,9 +11,11 @@ from .llm import (
11
11
  from .providers import (
12
12
  AnthropicRegexBuilder,
13
13
  CerebrasRegexBuilder,
14
+ CohereRegexBuilder,
14
15
  DeepSeekRegexBuilder,
15
16
  FireworksRegexBuilder,
16
17
  GroqRegexBuilder,
18
+ MistralRegexBuilder,
17
19
  MoonshotRegexBuilder,
18
20
  OpenAIRegexBuilder,
19
21
  OpenRouterRegexBuilder,
@@ -32,9 +34,11 @@ __all__ = [
32
34
  "UnimplementedRegexBuilder",
33
35
  "AnthropicRegexBuilder",
34
36
  "CerebrasRegexBuilder",
37
+ "CohereRegexBuilder",
35
38
  "DeepSeekRegexBuilder",
36
39
  "FireworksRegexBuilder",
37
40
  "GroqRegexBuilder",
41
+ "MistralRegexBuilder",
38
42
  "MoonshotRegexBuilder",
39
43
  "OpenAIRegexBuilder",
40
44
  "OpenRouterRegexBuilder",
@@ -1,8 +1,10 @@
1
1
  from .anthropic import AnthropicRegexBuilder
2
2
  from .cerebras import CerebrasRegexBuilder
3
+ from .cohere import CohereRegexBuilder
3
4
  from .deepseek import DeepSeekRegexBuilder
4
5
  from .fireworks import FireworksRegexBuilder
5
6
  from .groq import GroqRegexBuilder
7
+ from .mistral import MistralRegexBuilder
6
8
  from .moonshot import MoonshotRegexBuilder
7
9
  from .openai import OpenAIRegexBuilder
8
10
  from .openrouter import OpenRouterRegexBuilder
@@ -13,9 +15,11 @@ from .xai import XAIRegexBuilder
13
15
  __all__ = [
14
16
  "AnthropicRegexBuilder",
15
17
  "CerebrasRegexBuilder",
18
+ "CohereRegexBuilder",
16
19
  "DeepSeekRegexBuilder",
17
20
  "FireworksRegexBuilder",
18
21
  "GroqRegexBuilder",
22
+ "MistralRegexBuilder",
19
23
  "MoonshotRegexBuilder",
20
24
  "OpenAIRegexBuilder",
21
25
  "OpenRouterRegexBuilder",
@@ -0,0 +1,173 @@
1
+ """Cohere-backed RegexBuilder implementation.
2
+
3
+ Like Mistral, Cohere's official Python SDK is not built on the OpenAI
4
+ client library — it has its own generated client shape. Unlike Mistral's
5
+ single client (which exposes both complete()/complete_async() on one
6
+ object), Cohere exposes genuinely separate ``ClientV2``/``AsyncClientV2``
7
+ classes; this builder only ever needs the sync ``ClientV2``, since
8
+ ``RegexBuilder.build_pattern()`` is a synchronous call.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import time
15
+ from typing import TYPE_CHECKING, Any, cast
16
+
17
+ from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
18
+ from .cost import estimate_cost
19
+ from .models import default_model
20
+ from .text import extract_pattern
21
+
22
+ if TYPE_CHECKING:
23
+ import cohere as cohere_sdk
24
+
25
+ DEFAULT_MODEL = default_model("cohere")
26
+
27
+ _DEFAULT_MAX_TOKENS = 1024
28
+
29
+ # HTTP statuses where retrying the exact same request can't succeed —
30
+ # mirrors providers/errors.py's NON_RETRYABLE class-name set (BadRequest,
31
+ # Authentication, PermissionDenied, NotFound, Conflict, RequestTooLarge,
32
+ # UnprocessableEntity). The cohere SDK raises a family of named exception
33
+ # classes (BadRequestError, UnauthorizedError, ForbiddenError, ...), but
34
+ # those names don't line up with OpenAI/Anthropic's naming, and every one
35
+ # of them carries its status on exc.status_code (see
36
+ # cohere.core.api_error.ApiError, their shared base) — so classification
37
+ # here is done by status code instead of exception class name.
38
+ _NON_RETRYABLE_STATUSES = frozenset({400, 401, 403, 404, 409, 413, 422})
39
+
40
+
41
+ def _import_cohere() -> Any:
42
+ """Deferred import — cohere is an optional extra (parseforge[cohere]);
43
+ nothing else in this module (or a cache-hit lookup, which never reaches
44
+ build_pattern at all — see resolver.cli_name) should require it to be
45
+ installed."""
46
+ try:
47
+ import cohere
48
+ import cohere.core.api_error # noqa: F401 (makes cohere.core.api_error.ApiError reachable off the returned module)
49
+ except ImportError as exc:
50
+ raise ImportError(
51
+ "the cohere package is required to use CohereRegexBuilder — "
52
+ "install it via `pip install parseforge[cohere]`"
53
+ ) from exc
54
+ return cohere
55
+
56
+
57
+ def _status_code(exc: Any) -> int | None:
58
+ return getattr(exc, "status_code", None)
59
+
60
+
61
+ def _is_retryable(exc: Any) -> bool:
62
+ return _status_code(exc) not in _NON_RETRYABLE_STATUSES
63
+
64
+
65
+ def _format_error_reason(exc: Any) -> str:
66
+ return f"LLM-ERROR-cohere_sdk-{_status_code(exc)}-{exc}"
67
+
68
+
69
+ def _extract_text(content: Any) -> str:
70
+ """Cohere's AssistantMessageResponse.content is a list of content
71
+ blocks (text and/or thinking blocks), unlike Mistral/OpenAI's plain
72
+ string — join the text blocks' .text, same as textfsm-ai's own
73
+ CohereProvider._parse_cohere_response()."""
74
+ if isinstance(content, str):
75
+ return content
76
+ return "".join(getattr(block, "text", "") for block in content or [])
77
+
78
+
79
+ class CohereRegexBuilder:
80
+ """Builds a cli-name regex pattern by prompting a Cohere-hosted model.
81
+
82
+ The client is constructed lazily, on the first actual call — not in
83
+ ``__init__`` — so this can be used as a default RegexBuilder without
84
+ requiring ``COHERE_API_KEY`` to be set for cache-hit lookups, which
85
+ never reach the LLM at all (see resolver.cli_name).
86
+
87
+ API key resolves from the ``api_key`` argument if given, otherwise the
88
+ ``COHERE_API_KEY`` environment variable (the SDK's own default).
89
+ """
90
+
91
+ provider = "cohere"
92
+
93
+ def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
94
+ self.model = model
95
+ self._api_key = api_key
96
+ self._client: cohere_sdk.ClientV2 | None = None
97
+
98
+ def _get_client(self) -> cohere_sdk.ClientV2:
99
+ if self._client is None:
100
+ cohere = _import_cohere()
101
+ api_key = self._api_key or os.environ.get("COHERE_API_KEY")
102
+ if not api_key:
103
+ raise RuntimeError(
104
+ "no Cohere API key — pass api_key or set COHERE_API_KEY"
105
+ )
106
+ self._client = cohere.ClientV2(api_key=api_key)
107
+ return self._client
108
+
109
+ def build_pattern(
110
+ self, command: str, context: CliContext, **kwargs: Any
111
+ ) -> LLMCLIResponse:
112
+ cohere = _import_cohere()
113
+ prompt = build_prompt(command, context)
114
+ max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
115
+
116
+ start = time.monotonic()
117
+ try:
118
+ response = self._get_client().chat(
119
+ model=self.model,
120
+ max_tokens=max_tokens,
121
+ # cast: cohere's generated stubs want a precise
122
+ # UserChatMessageV2/... union or matching TypedDict, but a
123
+ # plain {"role": "user", "content": ...} dict is exactly
124
+ # that shape.
125
+ messages=cast(Any, [{"role": "user", "content": prompt}]),
126
+ **kwargs,
127
+ )
128
+ except cohere.core.api_error.ApiError as exc:
129
+ if not _is_retryable(exc):
130
+ # Same request would fail the same way again — stop rather
131
+ # than let a caller burn another attempt on it.
132
+ raise
133
+ return LLMCLIResponse(
134
+ content="",
135
+ raw=exc,
136
+ usage=TokenUsage(
137
+ input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
138
+ ),
139
+ duration_ms=(time.monotonic() - start) * 1000,
140
+ reason=_format_error_reason(exc),
141
+ ready=False,
142
+ )
143
+ duration_ms = (time.monotonic() - start) * 1000
144
+
145
+ text = _extract_text(response.message.content)
146
+ # usage/usage.tokens are both Optional, and input_tokens/
147
+ # output_tokens are typed float — unlike every openai-compat
148
+ # provider, Cohere's usage.tokens has no total_tokens field at
149
+ # all, so it's computed here rather than read off the response.
150
+ tokens = response.usage.tokens if response.usage else None
151
+ input_tokens = int(tokens.input_tokens or 0) if tokens else 0
152
+ output_tokens = int(tokens.output_tokens or 0) if tokens else 0
153
+ total_tokens = input_tokens + output_tokens
154
+
155
+ return LLMCLIResponse(
156
+ content=extract_pattern(text),
157
+ raw=response,
158
+ usage=TokenUsage(
159
+ input_tokens=input_tokens,
160
+ output_tokens=output_tokens,
161
+ total_tokens=total_tokens,
162
+ estimated_cost=estimate_cost(
163
+ input_tokens=input_tokens,
164
+ output_tokens=output_tokens,
165
+ total_tokens=total_tokens,
166
+ provider=self.provider,
167
+ model=self.model,
168
+ ),
169
+ ),
170
+ duration_ms=duration_ms,
171
+ reason=response.finish_reason or "",
172
+ ready=response.finish_reason == "COMPLETE",
173
+ )
@@ -0,0 +1,163 @@
1
+ """Mistral AI-backed RegexBuilder implementation.
2
+
3
+ Unlike the OpenAI-compatible providers (DeepSeek/Groq/xAI/Together/
4
+ Fireworks/Perplexity/OpenRouter/Moonshot/Cerebras), Mistral's official
5
+ Python SDK is not built on the OpenAI client library — it has its own
6
+ generated client shape (``client.chat.complete()``), even though the
7
+ underlying response shape (``choices[0].message.content``,
8
+ ``usage.prompt_tokens/completion_tokens/total_tokens``) closely mirrors
9
+ OpenAI's chat-completions format. So this talks to the native
10
+ ``mistralai`` SDK directly rather than pointing the ``openai`` SDK at a
11
+ different base_url.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import time
18
+ from typing import TYPE_CHECKING, Any, cast
19
+
20
+ from ..llm import CliContext, LLMCLIResponse, TokenUsage, build_prompt
21
+ from .cost import estimate_cost
22
+ from .models import default_model
23
+ from .text import extract_pattern
24
+
25
+ if TYPE_CHECKING:
26
+ import mistralai as mistralai_sdk
27
+
28
+ DEFAULT_MODEL = default_model("mistral")
29
+
30
+ _DEFAULT_MAX_TOKENS = 1024
31
+
32
+ # HTTP statuses where retrying the exact same request can't succeed —
33
+ # mirrors providers/errors.py's NON_RETRYABLE class-name set (BadRequest,
34
+ # Authentication, PermissionDenied, NotFound, Conflict, RequestTooLarge,
35
+ # UnprocessableEntity), but the mistralai SDK raises a single SDKError for
36
+ # every HTTP failure (status carried on exc.raw_response.status_code)
37
+ # rather than a family of named exception classes, so classification here
38
+ # is done by status code instead of exception class name.
39
+ _NON_RETRYABLE_STATUSES = frozenset({400, 401, 403, 404, 409, 413, 422})
40
+
41
+
42
+ def _import_mistralai() -> Any:
43
+ """Deferred import — mistralai is an optional extra (parseforge[mistral]);
44
+ nothing else in this module (or a cache-hit lookup, which never reaches
45
+ build_pattern at all — see resolver.cli_name) should require it to be
46
+ installed."""
47
+ try:
48
+ import mistralai
49
+ except ImportError as exc:
50
+ raise ImportError(
51
+ "the mistralai package is required to use MistralRegexBuilder — "
52
+ "install it via `pip install parseforge[mistral]`"
53
+ ) from exc
54
+ return mistralai
55
+
56
+
57
+ def _status_code(exc: Any) -> int | None:
58
+ return getattr(getattr(exc, "raw_response", None), "status_code", None)
59
+
60
+
61
+ def _is_retryable(exc: Any) -> bool:
62
+ return _status_code(exc) not in _NON_RETRYABLE_STATUSES
63
+
64
+
65
+ def _format_error_reason(exc: Any) -> str:
66
+ return f"LLM-ERROR-mistral_sdk-{_status_code(exc)}-{exc}"
67
+
68
+
69
+ class MistralRegexBuilder:
70
+ """Builds a cli-name regex pattern by prompting a Mistral-hosted model.
71
+
72
+ The client is constructed lazily, on the first actual call — not in
73
+ ``__init__`` — so this can be used as a default RegexBuilder without
74
+ requiring ``MISTRAL_API_KEY`` to be set for cache-hit lookups, which
75
+ never reach the LLM at all (see resolver.cli_name).
76
+
77
+ API key resolves from the ``api_key`` argument if given, otherwise the
78
+ ``MISTRAL_API_KEY`` environment variable (the SDK's own default).
79
+ """
80
+
81
+ provider = "mistral"
82
+
83
+ def __init__(self, model: str = DEFAULT_MODEL, api_key: str | None = None) -> None:
84
+ self.model = model
85
+ self._api_key = api_key
86
+ self._client: mistralai_sdk.Mistral | None = None
87
+
88
+ def _get_client(self) -> mistralai_sdk.Mistral:
89
+ if self._client is None:
90
+ mistralai = _import_mistralai()
91
+ api_key = self._api_key or os.environ.get("MISTRAL_API_KEY")
92
+ if not api_key:
93
+ raise RuntimeError(
94
+ "no Mistral API key — pass api_key or set MISTRAL_API_KEY"
95
+ )
96
+ self._client = mistralai.Mistral(api_key=api_key)
97
+ return self._client
98
+
99
+ def build_pattern(
100
+ self, command: str, context: CliContext, **kwargs: Any
101
+ ) -> LLMCLIResponse:
102
+ mistralai = _import_mistralai()
103
+ prompt = build_prompt(command, context)
104
+ max_tokens = kwargs.pop("max_tokens", None) or _DEFAULT_MAX_TOKENS
105
+
106
+ start = time.monotonic()
107
+ try:
108
+ response = self._get_client().chat.complete(
109
+ model=self.model,
110
+ max_tokens=max_tokens,
111
+ # cast: mistralai's generated stubs want a precise
112
+ # AssistantMessage/SystemMessage/... union or matching
113
+ # TypedDict, but a plain {"role": "user", "content": ...}
114
+ # dict is exactly the shape UserMessageTypedDict expects.
115
+ messages=cast(Any, [{"role": "user", "content": prompt}]),
116
+ **kwargs,
117
+ )
118
+ except mistralai.models.SDKError as exc:
119
+ if not _is_retryable(exc):
120
+ # Same request would fail the same way again — stop rather
121
+ # than let a caller burn another attempt on it.
122
+ raise
123
+ return LLMCLIResponse(
124
+ content="",
125
+ raw=exc,
126
+ usage=TokenUsage(
127
+ input_tokens=0, output_tokens=0, total_tokens=0, estimated_cost=0.0
128
+ ),
129
+ duration_ms=(time.monotonic() - start) * 1000,
130
+ reason=_format_error_reason(exc),
131
+ ready=False,
132
+ )
133
+ duration_ms = (time.monotonic() - start) * 1000
134
+
135
+ choice = response.choices[0]
136
+ content = choice.message.content
137
+ text = content if isinstance(content, str) else ""
138
+ # prompt_tokens/completion_tokens/total_tokens are typed
139
+ # Optional[int] = 0 in Mistral's UsageInfo, even though the API
140
+ # always populates them — guard against the type, not just the
141
+ # runtime default.
142
+ input_tokens = response.usage.prompt_tokens or 0
143
+ output_tokens = response.usage.completion_tokens or 0
144
+ total_tokens = response.usage.total_tokens or 0
145
+ return LLMCLIResponse(
146
+ content=extract_pattern(text),
147
+ raw=response,
148
+ usage=TokenUsage(
149
+ input_tokens=input_tokens,
150
+ output_tokens=output_tokens,
151
+ total_tokens=total_tokens,
152
+ estimated_cost=estimate_cost(
153
+ input_tokens=input_tokens,
154
+ output_tokens=output_tokens,
155
+ total_tokens=total_tokens,
156
+ provider=self.provider,
157
+ model=self.model,
158
+ ),
159
+ ),
160
+ duration_ms=duration_ms,
161
+ reason=choice.finish_reason or "",
162
+ ready=choice.finish_reason == "stop",
163
+ )
@@ -110,3 +110,22 @@ cerebras:
110
110
  - qwen-3-32b
111
111
  - gpt-oss-120b
112
112
  deprecated: []
113
+
114
+ mistral:
115
+ default: mistral-small-latest
116
+ supported:
117
+ - mistral-small-latest
118
+ - mistral-medium-latest
119
+ - mistral-large-latest
120
+ - magistral-medium-latest
121
+ - codestral-latest
122
+ deprecated: []
123
+
124
+ cohere:
125
+ default: command-light
126
+ supported:
127
+ - command-light
128
+ - command-r
129
+ - command-r-plus
130
+ - command-a
131
+ deprecated: []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parseforge
3
- Version: 0.2.6
3
+ Version: 0.2.8
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>
@@ -60,6 +60,12 @@ Requires-Dist: textfsm-ai[moonshot]>=0.6.1; extra == "moonshot"
60
60
  Provides-Extra: cerebras
61
61
  Requires-Dist: openai>=1.0.0; extra == "cerebras"
62
62
  Requires-Dist: textfsm-ai[cerebras]>=0.6.1; extra == "cerebras"
63
+ Provides-Extra: mistral
64
+ Requires-Dist: mistralai==1.10.0; extra == "mistral"
65
+ Requires-Dist: textfsm-ai[mistral]>=0.6.1; extra == "mistral"
66
+ Provides-Extra: cohere
67
+ Requires-Dist: cohere==5.21.1; extra == "cohere"
68
+ Requires-Dist: textfsm-ai[cohere]>=0.6.1; extra == "cohere"
63
69
  Provides-Extra: sampling
64
70
  Requires-Dist: netmiko>=4.0; extra == "sampling"
65
71
  Provides-Extra: dev
@@ -70,6 +76,8 @@ Requires-Dist: black>=24.0.0; extra == "dev"
70
76
  Requires-Dist: mypy>=1.10.0; extra == "dev"
71
77
  Requires-Dist: anthropic>=0.40.0; extra == "dev"
72
78
  Requires-Dist: openai>=1.0.0; extra == "dev"
79
+ Requires-Dist: mistralai==1.10.0; extra == "dev"
80
+ Requires-Dist: cohere==5.21.1; extra == "dev"
73
81
  Provides-Extra: release
74
82
  Requires-Dist: bump2version; extra == "release"
75
83
  Requires-Dist: build; extra == "release"
@@ -112,11 +120,11 @@ command that's pure local processing (`canonical`/`readable`/`recognizers`,
112
120
  LLM (`name`, `check --provider`, `run`, `generate-template`, `trial`) needs the
113
121
  extra for whichever provider it uses: `anthropic`, `openai`, `deepseek`,
114
122
  `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]`.
123
+ `moonshot`, `cerebras`, `mistral`, or `cohere`. `--provider` defaults to
124
+ `anthropic` wherever it isn't required, so that's the one most setups need.
125
+ `pip install parseforge[sampling]` adds Netmiko for live device sampling;
126
+ combine extras as needed, e.g.
127
+ `pip install parseforge[anthropic,openai,deepseek,groq,xai,together,fireworks,perplexity,openrouter,moonshot,cerebras,mistral,cohere,sampling]`.
120
128
 
121
129
  ## Development
122
130
 
@@ -124,11 +132,13 @@ as needed, e.g.
124
132
  pip install -e ".[dev,sampling]"
125
133
  pytest
126
134
  ```
127
- `dev` already includes both the `anthropic` and `openai` SDKs (tests exercise
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`.
135
+ `dev` already includes the `anthropic`, `openai`, `mistralai`, and `cohere`
136
+ SDKs (tests exercise all thirteen providers — `anthropic`, `openai`,
137
+ `deepseek`, `groq`, `xai`, `together`, `fireworks`, `perplexity`,
138
+ `openrouter`, `moonshot`, `cerebras` share just the first two packages, and
139
+ `mistral`/`cohere` each need their own native SDK and never silently
140
+ skip) — add the specific `,<provider>` extra explicitly only if installing
141
+ outside of `dev`.
132
142
 
133
143
  Linting/formatting/type-checking/docs run through tox instead of extras — see
134
144
  `tox.ini` (`tox -e lint`/`format`/`typecheck`/`docs`), each installing its own
@@ -30,11 +30,13 @@ parseforge/naming/resolver.py
30
30
  parseforge/naming/providers/__init__.py
31
31
  parseforge/naming/providers/anthropic.py
32
32
  parseforge/naming/providers/cerebras.py
33
+ parseforge/naming/providers/cohere.py
33
34
  parseforge/naming/providers/cost.py
34
35
  parseforge/naming/providers/deepseek.py
35
36
  parseforge/naming/providers/errors.py
36
37
  parseforge/naming/providers/fireworks.py
37
38
  parseforge/naming/providers/groq.py
39
+ parseforge/naming/providers/mistral.py
38
40
  parseforge/naming/providers/models.py
39
41
  parseforge/naming/providers/models.yaml
40
42
  parseforge/naming/providers/moonshot.py
@@ -11,6 +11,10 @@ textfsm-ai[anthropic]>=0.6.1
11
11
  openai>=1.0.0
12
12
  textfsm-ai[cerebras]>=0.6.1
13
13
 
14
+ [cohere]
15
+ cohere==5.21.1
16
+ textfsm-ai[cohere]>=0.6.1
17
+
14
18
  [deepseek]
15
19
  openai>=1.0.0
16
20
  textfsm-ai[deepseek]>=0.6.1
@@ -23,6 +27,8 @@ black>=24.0.0
23
27
  mypy>=1.10.0
24
28
  anthropic>=0.40.0
25
29
  openai>=1.0.0
30
+ mistralai==1.10.0
31
+ cohere==5.21.1
26
32
 
27
33
  [fireworks]
28
34
  openai>=1.0.0
@@ -32,6 +38,10 @@ textfsm-ai[fireworks]>=0.6.1
32
38
  openai>=1.0.0
33
39
  textfsm-ai[groq]>=0.6.1
34
40
 
41
+ [mistral]
42
+ mistralai==1.10.0
43
+ textfsm-ai[mistral]>=0.6.1
44
+
35
45
  [moonshot]
36
46
  openai>=1.0.0
37
47
  textfsm-ai[moonshot]>=0.6.1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "parseforge"
7
- version = "0.2.6"
7
+ version = "0.2.8"
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" }
@@ -73,6 +73,14 @@ perplexity = ["openai>=1.0.0", "textfsm-ai[perplexity]>=0.6.1"]
73
73
  openrouter = ["openai>=1.0.0", "textfsm-ai[openrouter]>=0.6.1"]
74
74
  moonshot = ["openai>=1.0.0", "textfsm-ai[moonshot]>=0.6.1"]
75
75
  cerebras = ["openai>=1.0.0", "textfsm-ai[cerebras]>=0.6.1"]
76
+ # Pinned exactly: 1.10.0 is the last mistralai release supporting Python
77
+ # 3.9 (1.10.1+ and all 2.x require Python >=3.10), matching textfsm-ai's
78
+ # own pin for the same reason -- this package also supports >=3.9.
79
+ mistral = ["mistralai==1.10.0", "textfsm-ai[mistral]>=0.6.1"]
80
+ # Pinned exactly for the same reason: 5.21.1 is the last cohere release
81
+ # supporting Python 3.9 (6.x/7.x require Python >=3.10), matching
82
+ # textfsm-ai's own pin.
83
+ cohere = ["cohere==5.21.1", "textfsm-ai[cohere]>=0.6.1"]
76
84
  sampling = ["netmiko>=4.0"]
77
85
  dev = [
78
86
  "pytest>=8.0.0",
@@ -80,11 +88,13 @@ dev = [
80
88
  "ruff>=0.4.0",
81
89
  "black>=24.0.0",
82
90
  "mypy>=1.10.0",
83
- # anthropic/openai are optional at runtime (see the extras above), but
84
- # both providers are exercised by the CLI/tests, which should never
85
- # silently skip -- so dev always pulls both in.
91
+ # anthropic/openai/mistralai/cohere are optional at runtime (see the
92
+ # extras above), but every provider is exercised by the CLI/tests,
93
+ # which should never silently skip -- so dev always pulls all four in.
86
94
  "anthropic>=0.40.0",
87
95
  "openai>=1.0.0",
96
+ "mistralai==1.10.0",
97
+ "cohere==5.21.1",
88
98
  ]
89
99
  # Tooling for scripts/release.ps1 and CI's own package-build step.
90
100
  # bump2version isn't otherwise installed anywhere -- release.ps1 assumes
@@ -1 +0,0 @@
1
- __version__ = "0.2.6"
File without changes
File without changes
File without changes
File without changes