Skip to content

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.

PLACEHOLDER module-attribute

PLACEHOLDER = '<set-this>'

logger module-attribute

logger = getLogger(__name__)

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

_USERNAME_CLAIM = 'https://iam.teradatacloud.io/token/claims/teradata_username'

_VS_SUFFIX module-attribute

_VS_SUFFIX = '/data-insights'

_DEFAULT_EMBEDDING_MODEL module-attribute

_DEFAULT_EMBEDDING_MODEL = 'amazon.titan-embed-text-v2:0'

NO_API_KEY module-attribute

NO_API_KEY = 'EMPTY'

_PRESERVED_SETTINGS module-attribute

_PRESERVED_SETTINGS = ('STRUCTURED_OUTPUT_METHOD', 'INSTRUCT_MODEL_CA_BUNDLE')

_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.

values instance-attribute

values = values

__init__

__init__(values)

__enter__

__enter__()

__exit__

__exit__(*exc_info)

_is_secret

_is_secret(attr)

True when attr names a credential that must never be written in clear.

_parse_dotenv

_parse_dotenv(path)

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_dotenv_value(value, *, force_quote=False)

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

_jwt_claims(token)

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.

_env

_env(name)

_render_value

_render_value(value, secret)

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

_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(label, *, secret=False, default=None, enabled=True)

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_insisting(label, *, secret=False, default=None, enabled=True, why=None, attempts=3)

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

_choose_auth(*, enabled=True, on_ai_studio=True)

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

_add_discovered(add, given, preserved, key)

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

_preserved_settings(path)

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

_tls_verifies(url, timeout=10.0)

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

_choose_llm(*, enabled=True, on_ai_studio=True, ingress_available=True)

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

_confirm(question, *, default=False, enabled=True)

Yes/no. Returns default when there is nobody to ask.

_suggest_model

_suggest_model(model_ids)

Pick the model to offer as the default from what the endpoint serves.

_list_models

_list_models(provider, url, api_key)

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

_choose_model(provider, url, api_key, *, enabled=True)

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

mcp_site_id(fqdn=None)

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.

_short_exc

_short_exc(exc)

detect_ai_studio

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(items, reveal)

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

_staged_values(items)

The {ATTR: value} a rendered item list would apply, live lines only.

_managed

_managed(attr)

_jwt_logon_failure

_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

_validate(items)

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

_warn_on_stale_session(items)

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_on_env_collisions(text)

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

_load_written(path)

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.