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.
- {parseforge-0.2.6/parseforge.egg-info → parseforge-0.2.8}/PKG-INFO +21 -11
- {parseforge-0.2.6 → parseforge-0.2.8}/README.md +12 -10
- parseforge-0.2.8/parseforge/__init__.py +1 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/cli/main.py +7 -4
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/__init__.py +4 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/__init__.py +4 -0
- parseforge-0.2.8/parseforge/naming/providers/cohere.py +173 -0
- parseforge-0.2.8/parseforge/naming/providers/mistral.py +163 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/models.yaml +19 -0
- {parseforge-0.2.6 → parseforge-0.2.8/parseforge.egg-info}/PKG-INFO +21 -11
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge.egg-info/SOURCES.txt +2 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge.egg-info/requires.txt +10 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/pyproject.toml +14 -4
- parseforge-0.2.6/parseforge/__init__.py +0 -1
- {parseforge-0.2.6 → parseforge-0.2.8}/LICENSE +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/MANIFEST.in +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/SPEC.md +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/cli/__init__.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/cli/config.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/drift.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/generation.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/integration.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/assemble.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/cache.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/llm.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/prompts.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/prompts.yaml +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/anthropic.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/cerebras.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/cost.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/deepseek.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/errors.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/fireworks.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/groq.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/models.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/moonshot.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/openai.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/openrouter.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/perplexity.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/text.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/together.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/providers/xai.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/naming/resolver.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/paths.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/pipeline.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/promotion.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/sampling/__init__.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/sampling/backends/__init__.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/sampling/backends/netmiko.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/sampling/core.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge/validation.py +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge.egg-info/dependency_links.txt +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge.egg-info/entry_points.txt +0 -0
- {parseforge-0.2.6 → parseforge-0.2.8}/parseforge.egg-info/top_level.txt +0 -0
- {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.
|
|
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 `
|
|
116
|
-
isn't required, so that's the one most setups need.
|
|
117
|
-
parseforge[sampling]` adds Netmiko for live device sampling;
|
|
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
|
|
128
|
-
all
|
|
129
|
-
`
|
|
130
|
-
share just
|
|
131
|
-
|
|
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 `
|
|
39
|
-
isn't required, so that's the one most setups need.
|
|
40
|
-
parseforge[sampling]` adds Netmiko for live device sampling;
|
|
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
|
|
51
|
-
all
|
|
52
|
-
`
|
|
53
|
-
share just
|
|
54
|
-
|
|
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
|
|
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"
|
|
283
|
-
"uses its own separate --naming-provider,
|
|
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.
|
|
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 `
|
|
116
|
-
isn't required, so that's the one most setups need.
|
|
117
|
-
parseforge[sampling]` adds Netmiko for live device sampling;
|
|
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
|
|
128
|
-
all
|
|
129
|
-
`
|
|
130
|
-
share just
|
|
131
|
-
|
|
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.
|
|
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
|
|
84
|
-
#
|
|
85
|
-
# silently skip -- so dev always pulls
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|