mathslate 0.1.0__py3-none-any.whl
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.
- mathslate/__init__.py +140 -0
- mathslate/_text.py +64 -0
- mathslate/ai/__init__.py +51 -0
- mathslate/ai/providers.py +231 -0
- mathslate/ai/suggest.py +453 -0
- mathslate/api.py +626 -0
- mathslate/classroom.py +229 -0
- mathslate/codegen.py +843 -0
- mathslate/core/__init__.py +24 -0
- mathslate/core/_budget.py +334 -0
- mathslate/core/_failure.py +85 -0
- mathslate/core/_sets.py +170 -0
- mathslate/core/_source.py +48 -0
- mathslate/core/analysis.py +1277 -0
- mathslate/core/binding.py +182 -0
- mathslate/core/data.py +775 -0
- mathslate/core/dispatch.py +1106 -0
- mathslate/core/sampling.py +947 -0
- mathslate/core/surfaces.py +305 -0
- mathslate/core/tables.py +202 -0
- mathslate/errors.py +43 -0
- mathslate/render/__init__.py +7 -0
- mathslate/render/axes.py +126 -0
- mathslate/render/options.py +199 -0
- mathslate/render/plotly_backend.py +580 -0
- mathslate/result.py +842 -0
- mathslate/ui/__init__.py +16 -0
- mathslate/ui/adapters.py +104 -0
- mathslate/ui/interact.py +302 -0
- mathslate-0.1.0.dist-info/METADATA +338 -0
- mathslate-0.1.0.dist-info/RECORD +33 -0
- mathslate-0.1.0.dist-info/WHEEL +4 -0
- mathslate-0.1.0.dist-info/licenses/LICENSE +21 -0
mathslate/__init__.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""MathSlate — a mathematical workspace that grows with you.
|
|
2
|
+
|
|
3
|
+
from mathslate import *
|
|
4
|
+
plot(sin(x)/x)
|
|
5
|
+
|
|
6
|
+
Everything symbolic is SymPy's, unwrapped and re-exported. MathSlate adds
|
|
7
|
+
eight callables and one guarantee: the graph is right, and you can always ask
|
|
8
|
+
it what the plain Python was.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
# --- SymPy re-exports (zero MathSlate code) -------------------------------
|
|
14
|
+
# PRD 6.3: thin wrappers around identically named SymPy functions are not
|
|
15
|
+
# wrapped at all. What follows is a plain re-export.
|
|
16
|
+
import sympy as sympy
|
|
17
|
+
from sympy import ( # noqa: F401
|
|
18
|
+
E,
|
|
19
|
+
Abs,
|
|
20
|
+
Eq,
|
|
21
|
+
Float,
|
|
22
|
+
Function,
|
|
23
|
+
I,
|
|
24
|
+
Integer,
|
|
25
|
+
Matrix,
|
|
26
|
+
Piecewise,
|
|
27
|
+
Rational,
|
|
28
|
+
Sum,
|
|
29
|
+
Symbol,
|
|
30
|
+
acos,
|
|
31
|
+
apart,
|
|
32
|
+
asin,
|
|
33
|
+
atan,
|
|
34
|
+
binomial,
|
|
35
|
+
cancel,
|
|
36
|
+
cbrt,
|
|
37
|
+
ceiling,
|
|
38
|
+
cos,
|
|
39
|
+
cosh,
|
|
40
|
+
cot,
|
|
41
|
+
csc,
|
|
42
|
+
diff,
|
|
43
|
+
exp,
|
|
44
|
+
expand,
|
|
45
|
+
factor,
|
|
46
|
+
factorial,
|
|
47
|
+
floor,
|
|
48
|
+
gcd,
|
|
49
|
+
integrate,
|
|
50
|
+
lambdify,
|
|
51
|
+
limit,
|
|
52
|
+
log,
|
|
53
|
+
nsimplify,
|
|
54
|
+
nsolve,
|
|
55
|
+
oo,
|
|
56
|
+
pi,
|
|
57
|
+
root,
|
|
58
|
+
sec,
|
|
59
|
+
series,
|
|
60
|
+
sign,
|
|
61
|
+
simplify,
|
|
62
|
+
sin,
|
|
63
|
+
sinh,
|
|
64
|
+
solve,
|
|
65
|
+
solveset,
|
|
66
|
+
sqrt,
|
|
67
|
+
symbols,
|
|
68
|
+
tan,
|
|
69
|
+
tanh,
|
|
70
|
+
together,
|
|
71
|
+
trigsimp,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
from . import errors as errors
|
|
75
|
+
from .api import (
|
|
76
|
+
analyze,
|
|
77
|
+
animate,
|
|
78
|
+
dataset,
|
|
79
|
+
frontend_report,
|
|
80
|
+
get_range_controls,
|
|
81
|
+
get_verbose,
|
|
82
|
+
plot,
|
|
83
|
+
polar,
|
|
84
|
+
set_range_controls,
|
|
85
|
+
set_verbose,
|
|
86
|
+
show_python,
|
|
87
|
+
slider,
|
|
88
|
+
table,
|
|
89
|
+
)
|
|
90
|
+
from .core.analysis import Analysis
|
|
91
|
+
from .core.data import Dataset
|
|
92
|
+
from .core.tables import Table
|
|
93
|
+
from .result import PlotResult
|
|
94
|
+
|
|
95
|
+
__version__: str = "0.1.0"
|
|
96
|
+
|
|
97
|
+
# --- predefined symbols (PRD 6.3) -----------------------------------------
|
|
98
|
+
# Open decision 3: `import *` is permitted so that these exist, but
|
|
99
|
+
# show_python() always emits the explicit `x = sp.symbols('x')`.
|
|
100
|
+
x, y, z, t, n, k = symbols("x y z t n k", real=True)
|
|
101
|
+
theta = Symbol("theta", real=True)
|
|
102
|
+
|
|
103
|
+
#: New public symbols introduced by MathSlate — the API-surface budget.
|
|
104
|
+
NEW_API: tuple[str, ...] = (
|
|
105
|
+
"plot",
|
|
106
|
+
"polar",
|
|
107
|
+
"slider",
|
|
108
|
+
"animate",
|
|
109
|
+
"table",
|
|
110
|
+
"analyze",
|
|
111
|
+
"show_python",
|
|
112
|
+
"dataset",
|
|
113
|
+
"set_verbose",
|
|
114
|
+
"get_verbose",
|
|
115
|
+
"set_range_controls",
|
|
116
|
+
"get_range_controls",
|
|
117
|
+
"frontend_report",
|
|
118
|
+
"PlotResult",
|
|
119
|
+
"Analysis",
|
|
120
|
+
"Table",
|
|
121
|
+
"Dataset",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
__all__ = [
|
|
125
|
+
# MathSlate
|
|
126
|
+
*NEW_API,
|
|
127
|
+
"errors",
|
|
128
|
+
"sympy",
|
|
129
|
+
"__version__",
|
|
130
|
+
# predefined symbols
|
|
131
|
+
"x", "y", "z", "t", "n", "k", "theta",
|
|
132
|
+
# SymPy re-exports
|
|
133
|
+
"E", "Abs", "Eq", "Float", "Function", "I", "Integer", "Matrix", "Piecewise",
|
|
134
|
+
"Rational", "Sum", "Symbol", "acos", "apart", "asin", "atan", "binomial",
|
|
135
|
+
"cancel", "cbrt", "ceiling", "cos", "cosh", "cot", "csc", "diff", "exp",
|
|
136
|
+
"expand", "factor", "factorial", "floor", "gcd", "integrate", "lambdify",
|
|
137
|
+
"limit", "log", "nsimplify", "nsolve", "oo", "pi", "root", "sec", "series",
|
|
138
|
+
"sign", "simplify", "sin", "sinh", "solve", "solveset", "sqrt", "symbols",
|
|
139
|
+
"tan", "tanh", "together", "trigsimp",
|
|
140
|
+
]
|
mathslate/_text.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Printing that survives a non-UTF-8 console.
|
|
2
|
+
|
|
3
|
+
MathSlate's inference report and π tick labels are nicer with real Unicode,
|
|
4
|
+
and notebooks render it happily. A Windows terminal on a legacy code page
|
|
5
|
+
(cp949, cp1252, …) raises ``UnicodeEncodeError`` instead — which would turn a
|
|
6
|
+
learner's very first ``plot()`` into a traceback. So: keep the Unicode, and
|
|
7
|
+
transliterate only when the destination cannot take it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Final, TextIO
|
|
14
|
+
|
|
15
|
+
__all__ = ["to_ascii", "encodable", "safe_print"]
|
|
16
|
+
|
|
17
|
+
_REPLACEMENTS: Final[tuple[tuple[str, str], ...]] = (
|
|
18
|
+
("∈", "in"), # ∈
|
|
19
|
+
("·", "-"), # ·
|
|
20
|
+
("—", "--"), # —
|
|
21
|
+
("–", "-"), # –
|
|
22
|
+
("→", "->"), # →
|
|
23
|
+
("π", "pi"), # π
|
|
24
|
+
("θ", "theta"), # θ
|
|
25
|
+
("φ", "phi"), # φ
|
|
26
|
+
("≤", "<="), # ≤
|
|
27
|
+
("≥", ">="), # ≥
|
|
28
|
+
("×", "x"), # ×
|
|
29
|
+
("∞", "inf"), # ∞
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def to_ascii(text: str) -> str:
|
|
34
|
+
"""Replace MathSlate's decorative Unicode with plain ASCII equivalents."""
|
|
35
|
+
for source, target in _REPLACEMENTS:
|
|
36
|
+
text = text.replace(source, target)
|
|
37
|
+
return text.encode("ascii", "replace").decode("ascii")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def encodable(text: str, stream: TextIO) -> bool:
|
|
41
|
+
"""Can ``stream`` represent ``text`` without loss?"""
|
|
42
|
+
encoding = getattr(stream, "encoding", None)
|
|
43
|
+
if not encoding:
|
|
44
|
+
return True
|
|
45
|
+
try:
|
|
46
|
+
text.encode(encoding)
|
|
47
|
+
except (UnicodeEncodeError, LookupError):
|
|
48
|
+
return False
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def safe_print(text: str, stream: TextIO | None = None) -> None:
|
|
53
|
+
"""``print`` that transliterates rather than raising or printing mojibake.
|
|
54
|
+
|
|
55
|
+
Checking up front matters: a console configured with ``errors='replace'``
|
|
56
|
+
does not raise, it silently prints question marks.
|
|
57
|
+
"""
|
|
58
|
+
target = stream if stream is not None else sys.stdout
|
|
59
|
+
if not encodable(text, target):
|
|
60
|
+
text = to_ascii(text)
|
|
61
|
+
try:
|
|
62
|
+
print(text, file=target)
|
|
63
|
+
except UnicodeEncodeError:
|
|
64
|
+
print(to_ascii(text), file=target)
|
mathslate/ai/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Natural language → MathSlate code (PRD 6.1, 7 v1.0).
|
|
2
|
+
|
|
3
|
+
from mathslate.ai import ask
|
|
4
|
+
print(ask("plot the tangent over one period").code)
|
|
5
|
+
|
|
6
|
+
Three rules, and the first two are absolute.
|
|
7
|
+
|
|
8
|
+
**The core never imports this package.** PRD §8 lists "AI dependency blocked on
|
|
9
|
+
school or corporate networks" as a risk whose mitigation is that the core must
|
|
10
|
+
work fully offline and the AI ships as extras. ``import mathslate`` does not
|
|
11
|
+
reach this module, and this module's provider libraries are imported lazily
|
|
12
|
+
inside the adapter that needs them, so nothing here can make a plain install
|
|
13
|
+
heavier or a school network a problem. ``tests/test_ai.py`` asserts it.
|
|
14
|
+
|
|
15
|
+
**It returns code; it does not run it.** A model writing Python and the library
|
|
16
|
+
executing it unseen is not something a learner can check, and checking is the
|
|
17
|
+
whole point of this project. :class:`Suggestion` holds the source and prints it.
|
|
18
|
+
An explicit :meth:`Suggestion.run` validates a small MathSlate-oriented AST,
|
|
19
|
+
uses restricted builtins, and runs under a wall-clock budget so an
|
|
20
|
+
allowlisted-but-runaway expression cannot hang the caller; unrestricted Python
|
|
21
|
+
requires ``unsafe=True``.
|
|
22
|
+
|
|
23
|
+
**No provider is bundled.** :func:`ask` resolves one at call time from what is
|
|
24
|
+
installed and configured, and says exactly what to install when it finds
|
|
25
|
+
nothing. Resolving PRD §9 open decision 4: the layer lives in the core package
|
|
26
|
+
as extras, not as a separate distribution — it is a few hundred lines and a
|
|
27
|
+
separate package would cost more in version skew than it saves.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from .providers import (
|
|
33
|
+
PROVIDERS,
|
|
34
|
+
Provider,
|
|
35
|
+
available_providers,
|
|
36
|
+
resolve_provider,
|
|
37
|
+
)
|
|
38
|
+
from .suggest import Suggestion, ask, configure, configured, forget, system_prompt
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"ask",
|
|
42
|
+
"configure",
|
|
43
|
+
"configured",
|
|
44
|
+
"forget",
|
|
45
|
+
"Suggestion",
|
|
46
|
+
"Provider",
|
|
47
|
+
"PROVIDERS",
|
|
48
|
+
"available_providers",
|
|
49
|
+
"resolve_provider",
|
|
50
|
+
"system_prompt",
|
|
51
|
+
]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""The three backends, behind one small protocol.
|
|
2
|
+
|
|
3
|
+
Each adapter is the least code that turns "a system prompt and a question" into
|
|
4
|
+
"some text". Provider SDKs are imported **inside** the call, never at module
|
|
5
|
+
scope, so importing :mod:`mathslate.ai` costs nothing and a missing SDK is a
|
|
6
|
+
sentence rather than an ImportError from three frames down.
|
|
7
|
+
|
|
8
|
+
Model names are defaults, not decisions: every provider moves faster than a
|
|
9
|
+
release cycle, so ``ask(..., model=...)`` overrides and the default is only
|
|
10
|
+
what a first-time caller gets.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Callable, Protocol
|
|
18
|
+
|
|
19
|
+
from ..errors import UnsupportedInputError
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Provider",
|
|
23
|
+
"Backend",
|
|
24
|
+
"PROVIDERS",
|
|
25
|
+
"available_providers",
|
|
26
|
+
"resolve_provider",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
#: How many tokens a suggestion may take. MathSlate answers are short.
|
|
30
|
+
MAX_TOKENS: int = 1500
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Backend(Protocol):
|
|
34
|
+
"""What a provider adapter has to be able to do."""
|
|
35
|
+
|
|
36
|
+
def complete(self, system: str, question: str, model: str) -> str:
|
|
37
|
+
"""Return the model's reply to ``question`` under ``system``."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Provider:
|
|
42
|
+
"""One named backend: what to install, what to set, and what it defaults to."""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
#: The import path, for detection.
|
|
46
|
+
package: str
|
|
47
|
+
#: What to type after `pip install` — not always the import path.
|
|
48
|
+
pip_name: str
|
|
49
|
+
env_var: str
|
|
50
|
+
default_model: str
|
|
51
|
+
build: Callable[[str | None], Backend]
|
|
52
|
+
|
|
53
|
+
def installed(self) -> bool:
|
|
54
|
+
import importlib.util
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
return importlib.util.find_spec(self.package) is not None
|
|
58
|
+
except (ImportError, ModuleNotFoundError, ValueError):
|
|
59
|
+
# `find_spec("google.genai")` raises rather than returning None when
|
|
60
|
+
# the parent package is absent, which is the ordinary case here.
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def configured(self) -> bool:
|
|
64
|
+
return bool(os.environ.get(self.env_var))
|
|
65
|
+
|
|
66
|
+
def ready(self) -> bool:
|
|
67
|
+
return self.installed() and self.configured()
|
|
68
|
+
|
|
69
|
+
def describe(self) -> str:
|
|
70
|
+
if not self.installed():
|
|
71
|
+
return f"{self.name}: not installed (pip install {self.pip_name})"
|
|
72
|
+
if not self.configured():
|
|
73
|
+
return f"{self.name}: installed, but {self.env_var} is not set"
|
|
74
|
+
return f"{self.name}: ready"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --------------------------------------------------------------------------
|
|
78
|
+
# adapters
|
|
79
|
+
# --------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _Anthropic:
|
|
83
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
84
|
+
self._api_key = api_key
|
|
85
|
+
|
|
86
|
+
def complete(self, system: str, question: str, model: str) -> str:
|
|
87
|
+
import anthropic
|
|
88
|
+
|
|
89
|
+
client = anthropic.Anthropic(api_key=self._api_key or None)
|
|
90
|
+
reply = client.messages.create(
|
|
91
|
+
model=model,
|
|
92
|
+
max_tokens=MAX_TOKENS,
|
|
93
|
+
system=system,
|
|
94
|
+
messages=[{"role": "user", "content": question}],
|
|
95
|
+
)
|
|
96
|
+
return "".join(
|
|
97
|
+
block.text for block in reply.content if getattr(block, "type", "") == "text"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class _OpenAI:
|
|
102
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
103
|
+
self._api_key = api_key
|
|
104
|
+
|
|
105
|
+
def complete(self, system: str, question: str, model: str) -> str:
|
|
106
|
+
import openai
|
|
107
|
+
|
|
108
|
+
client = openai.OpenAI(api_key=self._api_key or None)
|
|
109
|
+
reply = client.chat.completions.create(
|
|
110
|
+
model=model,
|
|
111
|
+
max_completion_tokens=MAX_TOKENS,
|
|
112
|
+
messages=[
|
|
113
|
+
{"role": "system", "content": system},
|
|
114
|
+
{"role": "user", "content": question},
|
|
115
|
+
],
|
|
116
|
+
)
|
|
117
|
+
return reply.choices[0].message.content or ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class _Gemini:
|
|
121
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
122
|
+
self._api_key = api_key
|
|
123
|
+
|
|
124
|
+
def complete(self, system: str, question: str, model: str) -> str:
|
|
125
|
+
from google import genai
|
|
126
|
+
from google.genai import types
|
|
127
|
+
|
|
128
|
+
client = genai.Client(api_key=self._api_key or None)
|
|
129
|
+
reply = client.models.generate_content(
|
|
130
|
+
model=model,
|
|
131
|
+
contents=question,
|
|
132
|
+
config=types.GenerateContentConfig(
|
|
133
|
+
system_instruction=system, max_output_tokens=MAX_TOKENS
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
return reply.text or ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
#: In preference order. Claude first because MathSlate's own development is
|
|
140
|
+
#: done against it, so its behaviour on this prompt is the best understood.
|
|
141
|
+
PROVIDERS: tuple[Provider, ...] = (
|
|
142
|
+
Provider(
|
|
143
|
+
name="claude",
|
|
144
|
+
package="anthropic",
|
|
145
|
+
pip_name="anthropic",
|
|
146
|
+
env_var="ANTHROPIC_API_KEY",
|
|
147
|
+
default_model="claude-sonnet-5",
|
|
148
|
+
build=_Anthropic,
|
|
149
|
+
),
|
|
150
|
+
Provider(
|
|
151
|
+
name="openai",
|
|
152
|
+
package="openai",
|
|
153
|
+
pip_name="openai",
|
|
154
|
+
env_var="OPENAI_API_KEY",
|
|
155
|
+
default_model="gpt-5",
|
|
156
|
+
build=_OpenAI,
|
|
157
|
+
),
|
|
158
|
+
Provider(
|
|
159
|
+
name="gemini",
|
|
160
|
+
package="google.genai",
|
|
161
|
+
pip_name="google-genai",
|
|
162
|
+
env_var="GOOGLE_API_KEY",
|
|
163
|
+
default_model="gemini-2.5-pro",
|
|
164
|
+
build=_Gemini,
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def available_providers() -> tuple[Provider, ...]:
|
|
170
|
+
"""Every provider that is both installed and configured."""
|
|
171
|
+
return tuple(provider for provider in PROVIDERS if provider.ready())
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def resolve_provider(
|
|
175
|
+
name: str | None = None, *, api_key: str | None = None
|
|
176
|
+
) -> Provider:
|
|
177
|
+
"""Pick a provider, or explain precisely what is missing.
|
|
178
|
+
|
|
179
|
+
An explicitly supplied key is a complete credential. Requiring the same
|
|
180
|
+
key to also exist in an environment variable would make ``ask(api_key=...)``
|
|
181
|
+
and ``configure(api_key=...)`` unusable in notebooks.
|
|
182
|
+
"""
|
|
183
|
+
if name is not None:
|
|
184
|
+
for provider in PROVIDERS:
|
|
185
|
+
if provider.name == name:
|
|
186
|
+
if not provider.installed():
|
|
187
|
+
raise UnsupportedInputError(
|
|
188
|
+
f"provider {name!r} needs its SDK: "
|
|
189
|
+
f"pip install {provider.pip_name}."
|
|
190
|
+
)
|
|
191
|
+
if not api_key and not provider.configured():
|
|
192
|
+
raise UnsupportedInputError(
|
|
193
|
+
f"provider {name!r} needs {provider.env_var} in the "
|
|
194
|
+
"environment or an explicit api_key=."
|
|
195
|
+
)
|
|
196
|
+
return provider
|
|
197
|
+
known = ", ".join(p.name for p in PROVIDERS)
|
|
198
|
+
raise UnsupportedInputError(f"unknown provider {name!r}; known: {known}.")
|
|
199
|
+
|
|
200
|
+
if api_key:
|
|
201
|
+
# A key is a credential for exactly one service, and nothing in the
|
|
202
|
+
# string reliably says which. Guessing would send the user's secret to
|
|
203
|
+
# a company it does not belong to, so an explicit key requires an
|
|
204
|
+
# explicit provider unless there is only one candidate.
|
|
205
|
+
candidates = tuple(p for p in PROVIDERS if p.installed())
|
|
206
|
+
if len(candidates) == 1:
|
|
207
|
+
return candidates[0]
|
|
208
|
+
if not candidates:
|
|
209
|
+
raise UnsupportedInputError(
|
|
210
|
+
"an api_key was given but no provider SDK is installed, so there "
|
|
211
|
+
"is nothing to send it to. Install one with "
|
|
212
|
+
"`pip install 'mathslate[ai]'`."
|
|
213
|
+
)
|
|
214
|
+
names = ", ".join(p.name for p in candidates)
|
|
215
|
+
raise UnsupportedInputError(
|
|
216
|
+
f"an explicit api_key needs an explicit provider: {names} are all "
|
|
217
|
+
"installed, and sending a key to the wrong one would hand your "
|
|
218
|
+
f"credential to the wrong company. Say which, e.g. "
|
|
219
|
+
f"provider='{candidates[0].name}'."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
ready = available_providers()
|
|
223
|
+
if ready:
|
|
224
|
+
return ready[0]
|
|
225
|
+
status = "\n ".join(provider.describe() for provider in PROVIDERS)
|
|
226
|
+
raise UnsupportedInputError(
|
|
227
|
+
"no AI provider is ready. MathSlate itself needs none of this and works "
|
|
228
|
+
"offline; the assistant is an optional extra.\n"
|
|
229
|
+
f" {status}\n"
|
|
230
|
+
"Install one with `pip install 'mathslate[ai]'` and set its API key."
|
|
231
|
+
)
|