tdfs4ds.ai_studio¶
tdfs4ds.ai_studio
¶
Generate a tdfs4ds .env from Teradata AI Studio's own environment
(v0.3.1.188).
Configuring tdfs4ds on AI Studio is the step that goes wrong, and it goes wrong quietly: a mistyped gateway URL or an unset variable surfaces much later as a model that "isn't configured" or a Vector Store that refuses a credential.
But AI Studio already publishes most of the answer. Comparing a known-working
configuration against a real notebook's os.environ, 16 of 22 variables
need no human input at all — 9 are derived from a platform variable and 7 are
constant for the platform, leaving 6 to ask about (and in password mode three of
those six are the same password, so it is asked once and fanned out).
Design notes¶
Why this is not in config.py. It encodes knowledge of one platform,
not of the configuration format. config.py must stay portable.
Why it does not route through export_config. That would inherit the
ordering and _serialize for free, but it would have to mutate
os.environ to stage the values — leaking platform config into the running
session as a side effect of writing a file — it emits every set variable in a
flat sort where this needs a curated commented template, and it emits no
quoting, which is the one thing this file must get right. _is_secret is
imported and reused -- it stays the single authority on what counts as a
credential -- while the rendering is this module's own.
Quoting is a correctness requirement. config._parse_dotenv handles a
quoted value and a trailing comment in an if/else: a quoted value is taken
verbatim, while an unquoted one loses everything from whitespace-then-#. A
password like abc #def would silently truncate, so every secret is written
quoted.
The unprefixed-variable rule is respected, not broken. tdfs4ds must never
read OAUTH_ACCESS_TOKEN (or any unprefixed platform name) at runtime — a
v0.3.1.154 decision, because a generic name can be owned by an unrelated
component and reading it would forward that component's credential with nothing
in the configuration naming it. This module is the tool that performs the
explicit, one-time, user-invoked mapping the docs already describe by hand:
it reads the platform name once and writes a named TDFS4DS_* variable,
so the resulting configuration states its own sources.
Off AI Studio it degrades and points elsewhere. detect_ai_studio()
returns an empty report rather than raising, the generated file marks its
AI-Studio-specific constants as assumptions, and the summary names
export_config(fmt='env', source='module') — which already generates a
.env from any working session on any platform and is the right tool there.
Public helpers¶
detect_ai_studio() -- which platform variables are present.
generate_ai_studio_env() -- write the .env, return a redacted preview.
PLATFORM_VARS
module-attribute
¶
PLATFORM_VARS = ('OAUTH_ACCESS_TOKEN', 'TD_LITELLM_BASE_URL', 'TD_VECTORSTORE_BASE_URL', 'TD_MCP_BASE_URL', 'JH_FQDN', 'TD_SSL_VERIFY', 'TD_AGENTOPS_BASE_URL', 'IDP_BASE_URL', 'IDP_CLIENT_ID', 'TD_KEYCLOAK_BASE_URL')
_USERNAME_CLAIM
module-attribute
¶
_DEFAULT_EMBEDDING_MODEL
module-attribute
¶
_PRESERVED_SETTINGS
module-attribute
¶
_MODEL_PREFERENCES
module-attribute
¶
_MODEL_PREFERENCES = (('ministral', '14b'), ('ministral',), ('mistral', 'large'), ('mistral',), ('claude',), ('gpt',))
MCP_IDP_URLS
module-attribute
¶
MCP_IDP_URLS = {'prod': 'https://login.customer.teradata.com', 'staging': 'https://login-stage.cloud.teradata.com', 'dev': 'https://login-dev.qacustomer.teradata.com'}
_MANAGED_PREFIXES
module-attribute
¶
_MANAGED_PREFIXES = ('TD_', 'VECTOR_STORE_', 'TERADATA_AI_', 'INSTRUCT_MODEL_', 'EMBEDDING_MODEL_', 'MCP_', 'STRUCTURED_OUTPUT_METHOD')
_Staged
¶
Bases: object
Apply a generated configuration to the session, then put it back.
So the configuration can be checked before the file is written: a
.env whose values do not work is worse than no file, because it looks
finished. tdfs4ds reads module attributes for most settings and
os.environ for the pass-through ones, so both are staged.
Variables the file omits are cleared, not inherited. Setting only what
the file contains checks the file layered on top of whatever the session
already had -- and a notebook that generated a .env an hour ago is
carrying all of it. That is how a run whose file correctly omitted
INSTRUCT_MODEL_VERIFY_SSL still reported TLS verification disabled: the
value came from the previous run, not from the file under test. Clearing is
scoped to :data:_MANAGED_PREFIXES so the user's own settings survive, and
it is undone on exit like everything else here.
Not everything is reversible, and the caller should know which: checking
calls configure_vector_store_auth(), which hands a token to teradataml
for the rest of the session, and a structured-output probe may leave the
process's learned method set. Those are the same side effects the checks
would have had after loading the file anyway -- they just happen slightly
earlier.
_is_secret
¶
True when attr names a credential that must never be written in clear.
_parse_dotenv
¶
Parse path as a .env file and return a {ATTR: raw_value} dict
for every TDFS4DS_* variable found.
Handles:
- KEY=VALUE
- KEY="VALUE" / KEY='VALUE'
- export KEY=VALUE
- # comments (whole-line and trailing) and blank lines
Trailing-comment rule, matching python-dotenv so a file behaves the same
whichever loader reads it: in an unquoted value, whitespace followed by
# starts a comment and is dropped. A # with no whitespace before it
is part of the value (PASSWORD=abc#def is abc#def), and quoting
keeps everything verbatim (PASSWORD="abc #def") — which is how to
write a value that genuinely contains " #".
This used to keep the comment: PASSWORD=secret # prod became the
literal secret # prod, which then failed authentication somewhere far
away from the file that caused it.
_render_dotenv_value
¶
Render one value for a .env line, quoting when it matters.
There is no escape syntax in :func:_parse_dotenv, so a value containing
both quote characters cannot be represented; that case is written bare with
a warning rather than silently corrupted.
force_quote is for values whose shape is not known in advance (a credential, typically): quoting one that did not need it is harmless, while failing to quote one that did truncates it at the first " #".
_jwt_claims
¶
Return a JWT's payload claims, or {} for anything unreadable.
Delegates to :mod:tdfs4ds.utils.tokens, which the diagnostics also use to
report token expiry — one decoder, since the two would otherwise disagree
about what counts as readable.
The signature is deliberately not verified: the only thing wanted here is the username the platform already authenticated, and this runs inside the session that was issued the token.
_render_value
¶
Render one value for a .env line.
The quoting itself lives in config, beside the parser whose rule it has
to satisfy. Only the placeholder short-circuit is this module's business.
_interactive
¶
True when there is somebody to ask a question of.
Not simply isatty(). A Jupyter kernel's sys.stdin is not a
terminal, yet input() and getpass() work there perfectly -- they
prompt in the notebook over the kernel's stdin channel. Gating on
isatty() alone meant the questions were silently skipped in exactly the
environment this generator exists for, and the file came out full of
placeholders with no prompt ever shown.
_ask
¶
Ask for one value. Returns None when unanswered -- never raises.
Declining, an empty answer, prompt=False and the absence of a terminal
are all the same outcome: the caller writes a placeholder and reports it.
_ask_insisting
¶
Ask for a value that the configuration genuinely needs, and insist.
A blank answer used to become <set-this> in the written file, which
does not work -- it loads as that literal string and fails somewhere else
entirely. So an empty answer is re-asked, with the reason it is needed,
before the caller is allowed to record a gap.
Returns None only after attempts empty answers, or immediately when
there is nobody to ask.
_choose_auth
¶
Ask how everything should authenticate, explaining what it costs.
This is the one choice in the file that cannot be derived and that changes how every component connects, so it is asked rather than defaulted silently. It is also not a free choice between equals: one trades a durable password on disk for a token that stops working in about two hours.
Returns 'password' (the safe default) when there is nobody to ask.
_add_discovered
¶
Emit a discovered setting: live when one is known, else a commented slot.
Order: an explicit override, then whatever the file being replaced already held, then nothing. The commented slot stays the default so a fresh file still documents the option without turning it on.
_preserved_settings
¶
Values to carry forward from the file that is about to be replaced.
Only the discovered ones (:data:_PRESERVED_SETTINGS), only when the file
already holds a real value — a placeholder or a blank carries nothing. An
explicit override still wins over whatever was there.
_tls_verifies
¶
Does this endpoint's certificate verify against the system trust store?
Returns True / False / None -- None meaning could not
tell (DNS failure, refused connection, timeout), which must never be read
as either answer.
Sends no credential: this is a handshake and nothing more.
It exists because TD_SSL_VERIFY is a platform-wide hint, in
practice about the in-cluster services and their own certificates, and
copying it onto a public ingress turns verification off for a certificate
that may well be signed by a public CA -- observed: an Entrust-issued
certificate, perfectly valid, with verification disabled by inheritance.
That leaves the connection encrypted but unauthenticated, which is the
half that does not protect an API key: anyone able to intercept can present
a certificate of their own. Choosing the TLS route and then not checking
the certificate defeats the reason for choosing it.
_choose_llm
¶
Ask which route the LLM traffic takes, explaining what it costs.
Asked for the same reason auth is: it cannot be derived. The platform publishes exactly one LiteLLM address and it is plaintext, so deriving means always choosing http:// -- and the cost of that choice (the API key and every prompt in clear) is invisible in the generated file, which just shows a URL. The alternative was only found by probing.
Returns 'incluster' -- the historical default -- when there is nobody
to ask or no ingress to offer.
_confirm
¶
Yes/no. Returns default when there is nobody to ask.
_suggest_model
¶
Pick the model to offer as the default from what the endpoint serves.
_list_models
¶
Return the model ids this endpoint serves, or [].
Reads through list_instruct_models(), which goes through the configured
proxy and TLS settings -- so it works on a gateway behind an internal CA,
where a bare request would not. Any failure is a missing convenience, never
an error: the caller falls back to asking for a name.
_choose_model
¶
Offer the endpoint's own model list, defaulting to the best match.
Typing a model name blind is the step most likely to be got wrong, and the endpoint can simply be asked. Falls back to a free-text question when the list cannot be fetched (no key yet, or the endpoint refuses to enumerate).
mcp_site_id
¶
The site id behind fqdn (default: JH_FQDN), or None.
tdicamworldpp69.private.preprod.cloud.teradata.com -> TDICAMWORLDPP69.
generate_mcp_token
¶
generate_mcp_token(*, idp_url=None, client_id=None, site_id=None, scope='openid profile email', timeout=300.0, open_browser=True)
Obtain an Enterprise MCP bearer token by OAuth 2.0 device flow.
The Enterprise MCP gateway on AI Studio does not accept the database username and password. It wants a bearer token from the platform's identity provider, and that token is obtained by the Device Authorization Grant: this prints a URL and a short code, you approve in a browser, and it polls until the token is issued.
That is why mcp='gateway' writes MCP_SERVER_HEADERS rather than
MCP_USERNAME/MCP_PASSWORD. Sending Basic credentials there gets the
database's own rejection -- Error 8017, The UserId, Password or Account
is invalid -- which reads like a wrong password and is really the wrong
kind of credential.
Parameters¶
idp_url : str, optional
Identity provider. Defaults to the production IdP; see
:data:MCP_IDP_URLS for the staging and dev ones.
client_id : str, optional
Defaults to onetd-<SITE_ID>.
site_id : str, optional
Defaults to the first label of JH_FQDN, upper-cased.
scope : str
OAuth scopes requested.
timeout : float
Seconds to keep polling before giving up.
open_browser : bool
Try to open the verification URL. The URL is printed either way.
Returns¶
str or None
The access token, or None if it could not be obtained. Never
raises: this is one input among several, and a failure here should
leave the caller free to carry on and paste a token by hand.
Notes¶
The token is not OAUTH_ACCESS_TOKEN. That one is the notebook
session's own token, issued for a different audience; this one is issued to
the onetd-<SITE> client for the MCP gateway.
detect_ai_studio
¶
Report which AI Studio platform variables are visible.
Never raises and never returns a credential value -- a token is reported as present with its decoded username, not as its own text.
Returns¶
dict
{'on_ai_studio': bool, 'found': [name, ...], 'missing': [name, ...],
'username': str or None}. on_ai_studio is True when any platform
variable is present; username is the database user decoded from the
token's claim.
_render
¶
Render the collected items as .env text.
reveal False replaces each secret value with set (hidden, N chars), so
the result is safe to print into a saved notebook. Both renderings come from
the same item list, so the preview cannot drift from the file.
generate_ai_studio_env
¶
generate_ai_studio_env(path=None, *, auth=None, token=None, mcp='gateway', mcp_token=None, llm=None, prompt=True, include_secrets=False, check=True, pin=False, force=False, **overrides)
Generate a tdfs4ds .env from AI Studio's platform environment.
Derives everything the platform exposes, asks for the rest, checks that the result actually works, and only then writes the file.
The order matters. An earlier version wrote first and offered to check
afterwards, filling anything unanswered with <set-this>; that produces
a file which looks finished, loads that literal string as a real value, and
fails much later somewhere that names neither the variable nor this
function. So now a blank answer is re-asked with the reason it is
needed, the configuration is validated before the write, and a file with
gaps or failing checks is written only if you say so.
Parameters¶
path : str, optional
Where to write. None writes nothing and only returns the preview.
tdfs4ds's .env discovery is first-match-wins (./.env, then
~/.tdfs4ds/.env), so a file written anywhere else has to be passed
to load_config(dotenv_path=...) explicitly.
auth : {'password', 'jwt', None}
None (default) asks, on AI Studio and with a terminal, laying
out the trade-off -- it is the one choice here that cannot be derived
and that changes how every component connects. Falls back to
'password' when there is nobody to ask.
``'password'`` writes a durable configuration: one database
password, asked once and reused for the Vector Store.
``'jwt'`` writes no password at all -- the platform token serves all
three -- but **the token expires about two hours after it was issued**,
so the file stops working and has to be regenerated. The unused variant
is emitted commented out either way.
token : str, optional
The token everything under auth='jwt' authenticates with — the
database logon (TD_LOGDATA), the Vector Store, and the MCP gateway.
**AI Studio hands out two tokens that look alike and are not
interchangeable.** The session's ``OAUTH_ACCESS_TOKEN`` authenticates
against the platform's own services; ``%gen_token``'s ``user_token`` is
minted for a **Vantage logon**, which is what the notebooks already
pass to ``create_context(logmech='JWT', logdata='token=...')``. Where
the platform token is not a database credential — confirmed on at least
one site — every component that opens a database session with it fails
with the database's own ``Error 8017``, and the gateway does exactly
that on the caller's behalf::
%gen_token
tdfs4ds.generate_ai_studio_env('.env', token=user_token)
Defaults to ``OAUTH_ACCESS_TOKEN``, which is right wherever the
platform token *is* database-capable. Under ``auth='jwt'`` the logon is
attempted before the file is written, so a token the database refuses
is reported instead of being discovered later as an unexplained 8017.
Ignored under ``auth='password'`` except for the MCP bearer.
mcp_token : str, optional The bearer for the Enterprise gateway alone, overriding token — use it when the gateway and the database want different tokens. There are two different tokens in AI Studio and only one is a database credential.
The gateway validates the bearer and then opens a Teradata session with
it -- which is why a failure there surfaces as the *database's*
``Error 8017`` rather than an HTTP 401. The session's
``OAUTH_ACCESS_TOKEN`` passes the first step and has been observed
failing the second, so where that happens supply the token minted for a
Vantage logon instead -- the one ``%gen_token`` leaves in
``user_token``, which the notebooks already use for
``create_context(logmech='JWT', logdata='token=...')``::
%gen_token
tdfs4ds.generate_ai_studio_env('.env', mcp_token=user_token)
Falls back to ``MCP_TOKEN`` from the environment, then to *token*, then
to :func:`generate_mcp_token`'s device flow.
mcp : {'gateway', 'incluster', None}
'gateway' (default) is https://{JH_FQDN}/one-td/mcp with
edition=enterprise, the verified combination. 'incluster' uses
TD_MCP_BASE_URL, which appears to be a Community-edition server (it
embeds its own credentials, so tdfs4ds sends none). None omits MCP.
llm : {'incluster', 'ingress', None}
Which route to the LiteLLM gateway. 'incluster' is the platform's
own TD_LITELLM_BASE_URL -- the shortest path, and the one an
AgentOps pod takes, but http://, so the API key and every prompt
travel in clear. 'ingress' is https://{JH_FQDN}/one-td/litellm,
the same one-td pattern as the MCP gateway, which adds TLS at the
cost of a hop. Whichever is not chosen is written commented out.
``None`` (default) **asks**, like *auth*: which route the traffic takes
is not derivable from the environment -- the platform publishes only
the plaintext one -- and defaulting to it silently is how a file ends
up sending an API key in clear without anyone choosing that. Without a
terminal it resolves to ``'incluster'``, the historical default.
On an ``http://`` URL ``INSTRUCT_MODEL_VERIFY_SSL`` is omitted entirely:
there is no TLS to verify, and setting it only builds a custom HTTP
client and warns on every call.
prompt : bool, default True Ask for values that cannot be derived, re-asking once or twice when the answer is blank. Without a terminal this is ignored and the unanswered values are reported as gaps instead, so the function stays usable from a script -- it just will not write a file it knows to be incomplete unless force says to.
Two answers are treated specially. The **instruct model** is chosen
from the list the endpoint itself serves (once the URL and key are
known), defaulting to a Ministral 14B if one is offered. The **API
key** is written as ``None`` when you have none, which loads as
``None`` -- correct for a gateway that does not require one, and honest
rather than a placeholder pretending to be a key.
include_secrets : bool, default False
The returned text renders secrets as set (hidden, N chars); the
file written to path always carries the real values. Pass True to
get the real text back -- then treat it as a secret.
check : bool, default True
Run describe_config() and check_configuration() before
writing, against the values staged into the session, and report what
fails. Failing checks block the write unless you confirm (or force).
Set False to skip, which is also what makes this function usable
offline.
One caveat: checking spends the credentials. ``set_auth_token`` is
called for real, so the Vector Store session is configured from here on
whether or not the file ends up written.
pin : bool, default False
After a successful write, record what the JSON-constraints check
discovered -- typically STRUCTURED_OUTPUT_METHOD -- into the file
just written, stamped with the endpoint and date. Requires check.
force : bool, default False
Write even with unanswered values or failing checks, without asking.
This is how a non-interactive script gets a file at all, and how the
tests do.
**overrides
Supply a value directly instead of deriving or asking for it, keyed by
the tdfs4ds variable name with or without the prefix -- e.g.
instruct_model_model='...' or TDFS4DS_TD_PASSWORD='...'.
Returns¶
str The rendered file, with secrets hidden unless include_secrets. It is returned whether or not the file was written, so a refused write still shows you exactly what it would have contained.
Notes¶
A successful write ends with load_config(dotenv_path=path), so the
running session is configured by the time this returns. Configuration is
otherwise read once, during import tdfs4ds -- a file written afterwards
would not reach the module, which is the load-order trap this package
documents everywhere. Doing it here means there is no step left to forget.
Examples¶
print(tdfs4ds.generate_ai_studio_env('.env')) # doctest: +SKIP tdfs4ds.generate_ai_studio_env('.env', auth='jwt') # doctest: +SKIP tdfs4ds.generate_ai_studio_env('.env', check=False, force=True) # doctest: +SKIP
_staged_values
¶
The {ATTR: value} a rendered item list would apply, live lines only.
_jwt_logon_failure
¶
Check the one credential check_configuration deliberately does not.
The database connection sits outside the AI-facing configuration, which is
why check_configuration leaves it alone -- a documented decision. But
under auth='jwt' this generator writes that credential, and on at
least one site the token it defaults to is accepted by every service and
refused by the database. The file is then written, every check passes, and
the first real call fails with an Error 8017 naming nothing the user
set. One logon attempt before writing is cheaper than debugging that.
Returns a failure row shaped like a check, or None when there is
nothing to say. The probe is inconclusive far more often than it is
negative -- an unreachable host, a driver that will not load -- and
"could not tell" must never be rendered as "refused".
_validate
¶
Run the configuration checks against what is about to be written.
Returns (ok, failures, recommend). Never raises -- a validation step
that crashes would block a file that might be perfectly good.
_warn_on_stale_session
¶
Name variables this session carries that the new file does not set.
They were checked as absent -- the staging above clears them -- but they are still live in THIS process, so the session and the file now disagree until the kernel restarts. Saying which ones is the difference between "why does it still say TLS is disabled?" and a one-line answer.
_warn_on_env_collisions
¶
Warn about pass-through variables the session already exports.
_apply_dotenv uses os.environ.setdefault for pass-through variables,
so one already exported in the session wins over the file -- which makes
a correct file look as though it had no effect.
_load_written
¶
Apply the file just written to the running session.
Configuration is read once, during import tdfs4ds, so a file written
afterwards has not reached the module -- the load-order trap this package
documents everywhere. Doing it here means the session is working by the
time this function returns, instead of after a step the reader has to know
about.