Deploy to Teradata AI Studio (AgentOps)¶
tdfs4ds can package its consumer agent for
AgentOps, the agent-hosting platform in Teradata AI Studio, so business
users reach it as a hosted service instead of a local Gradio app.
The package is derived from a working session. By the time you run this you have already proved your configuration by asking the agent questions; the generator snapshots that session rather than asking you to restate it.
One call¶
report = tdfs4ds.deploy_to_agentops(
'./teradata_agent',
database='MY_FEATURE_STORE', # required
databases_readonly=['LAB_STORE'], # optional
data_domain='CLAIMS',
token=user_token, auth='jwt', mcp=False,
progress=print)
[1/7] validate_target MY_FEATURE_STORE domain CLAIMS; 214 documented objects
[2/7] preflight ok - 14 checks
[3/7] publication ok - tdfs4ds on PyPI
[4/7] package 4 files, 9 runtime vars
[5/7] register published
[6/7] deploy building -> pushing -> deploying -> active
[7/7] verify 6/6 checks; startup confirmed
A failing stage stops the pipeline. dry_run=True stops after the zip and returns the payload —
run it first to confirm the store and that runtime_config is not empty.
database= is required
The feature store is a deployment target, not ambient state. Without an explicit argument the pod inherits whatever the session had configured, which is invisible at the call site. Model and Vector Store settings still come from the running session.
Deploy without MCP first
mcp=False omits only DATA_QUERY and ALGORITHM. It removes Enterprise auth and the
132-tool context budget from the run, so a failure is unambiguously about the agent.
The individual steps¶
import tdfs4ds
# 1. Before — is this session deployable?
tdfs4ds.check_agent_deployment(dest='./teradata_agent')
# 2. Inspect, then write.
print(tdfs4ds.generate_agentops_package())
result = tdfs4ds.write_agentops_package('./teradata_agent')
# 3. After — does the deployed agent actually work?
tdfs4ds.check_deployed_agent(agent_url, token=user_token)
Between steps 2 and 3, run the platform's own upload / poll / deploy cells. The deployment payload is generated for you:
What gets generated¶
AgentOps deploys from a zip holding four files plus the platform's
agent_runtime/ SDK.
agent_runtime is on no package index
The PyPI names agentops (an observability SDK) and agent_runtime (a
Microsoft AutoGen placeholder) are unrelated projects. Installing either
gives a successful build followed by an ImportError at pod start — never
put them in requirements.txt.
By default tdfs4ds generates a compatibility implementation of the
two-symbol contract when the destination has none, so no scaffold from the
tutorial folder is required. A platform-supplied copy always wins and is
never overwritten; agent_runtime='require' insists on it.
| File | Contents |
|---|---|
agent.py |
Platform entry point — AgentRuntimeApp, the @app.entrypoint handler |
customer_agent.py |
Agent logic: env mapping, one-time setup, the turn lock, thread_id derivation |
config.yaml |
Non-secret settings, committed with the archive |
requirements.txt |
Pinned tdfs4ds version |
Authentication modes¶
| Mode | When | How the pod connects |
|---|---|---|
service (default) |
A Teradata username/password exists | One long-lived context opened at import from TDFS4DS_TD_* |
jwt |
The platform authenticates with a token | Each caller's auth_context.token opens the session, lazily and per caller |
tdfs4ds.check_agent_deployment(auth='jwt')
result = tdfs4ds.write_agentops_package('./teradata_agent', auth='jwt')
Which one to use¶
Prefer jwt where the environment allows it — and note the reasoning is the
opposite of the notebook case, where a token expiring in two hours is exactly
what makes a .env stop working:
- the token arrives per request from the caller, so expiry self-heals instead of accumulating;
- the pod connects as the caller, so database permissions and audit reflect the real user rather than one shared account;
- no durable database password sits in
runtime_config, which is rendered in clear in the deployment UI. Its guarantee is "not committed to source control", not secrecy.
service remains the default because jwt is conditional on something
outside tdfs4ds: the platform's auth_context.token must be accepted by
Vantage as a JWT logon. A token minted for a platform API is not automatically
a database credential — this package has watched the MCP gateway accept a
platform bearer and the database then refuse the logon it attempted with it
(Error 8017). A pod cannot mint a token of its own, so where the platform's
token is not a valid Vantage JWT, jwt is not deployable at all and service
with a real account is the way in.
Settle it before deploying
Run this in the notebook, with the token the pod will be handed:
import os, teradataml as tdml
tdml.create_context(host=os.environ['TDFS4DS_TD_HOST'], logmech='JWT',
logdata='token=' + os.environ['OAUTH_ACCESS_TOKEN'])
A session means auth='jwt' is viable. A refusal means it is not, whatever
else is configured — and no amount of deployment configuration will change
that.
What jwt costs
Every caller needs SELECT rights on the feature store — there is no service
account behind the pod — and switching caller costs a reconnect, since
teradataml holds one process-wide context. In exchange, per-user
authorization is preserved and token expiry is self-healing: the next call
brings a fresh token and the context is rebuilt. Only a fingerprint of the
token is retained, never the token.
Where each setting goes¶
The split is derived from the same credential classification
export_config uses — never guessed, and never
duplicated.
| Destination | Contents | Why |
|---|---|---|
config.yaml |
ordinary settings (SCHEMA, DATA_DOMAIN, model names, …) |
bundled in the zip and committed, so it must be safe to read |
runtime_config |
credentials, TDFS4DS_TD_*, TDFS4DS_VECTOR_STORE_AUTH_*, TDFS4DS_MCP_USERNAME, TDFS4DS_MCP_AUTH |
injected at deploy time; connection variables identify a deployment, so committing them pins the package to wherever it was built |
| dropped | TDFS4DS_SERVE_*, and CHROMA_* on the Teradata backend |
the platform is the server, and a Chroma path in a pod is misleading |
config.yaml lists the omitted variable names as comments, so a later reader
can tell what the deployment must supply without the values being present.
runtime_config is not a vault
Its guarantee is "not committed to source control", which is weaker than confidential. The values are stored in the deployment record and shown in clear in the AgentOps UI, readable by anyone who can view the deployment. Treat a deploy as publishing those credentials and rotate them accordingly.
It still matters which side a value lands on: git history is permanent and
widely readable, whereas a deployment record can be deleted and its
credentials rotated. Putting a credential in config.yaml puts it in the
archive, for good.
Two MCP settings that carry no protected prefix
MCP_USERNAME and MCP_AUTH are routed to runtime_config by name, since
neither is a secret by its name and neither starts with TD_. MCP_USERNAME
is half a credential pair -- its partner MCP_PASSWORD is already a
credential, and MCP_USERNAME falls back to TDFS4DS_TD_USERNAME, so a
committed config.yaml would otherwise carry the database username in clear
while the password travelled separately. MCP_AUTH is an auth-mode
override, so a committed file could change how the pod authenticates.
With mcp=False every MCP_* setting is dropped before this applies, so
the common case is unaffected either way.
Connecting to Teradata from a pod¶
TDFS4DS_TD_HOST must be a name the pod can resolve, and often that is an IP address.
Verified on a live AgentOps deployment: dbc did not resolve from inside the pod, nor did any
qualified form - DNS for the database was unavailable there entirely. Setting TDFS4DS_TD_HOST to
the database's IP address was what worked.
The notebook is not evidence here. A bare name like dbc is completed by the resolver's search
domains, and an AgentOps pod need not share the notebook's - so the same value succeeds in one and
fails in the other, with nothing in the error pointing at a name:
[ERROR] reconnect_teradata: reconnecting to Teradata host=dbc user=DM5344095
[ERROR] reconnect_teradata: reconnection failed - (TDML_2000) Failed to connect to Teradata Vantage.
Read the elapsed time¶
TDML_2000 is identical for three unrelated faults. The gap between those two log lines separates
them - above it was 88 ms:
| Elapsed | Meaning | Fix |
|---|---|---|
| well under a second | the name never resolved | the IP address, or a qualified name that resolves |
| seconds, then failure | resolved but unreachable (routing, firewall) | a platform question, not a setting |
a round trip plus SQLSTATE 28000 |
reached and refused | the credential - auth= mode, password, or token audience |
Since v0.3.1.184 the generated agent prints this diagnosis itself, on the failure path only: the addresses a name resolved to or the resolver's verbatim error, the resolver's search domains with the bare name tried under each, and a TCP probe of port 1025 when a name does resolve. Read that before changing anything.
Two consequences of using an IP address, worth stating to whoever operates the deployment: it pins
the deployment to an address that can change, so re-check it if a working pod later stops
connecting; and it is independent of the auth mode - the identical host failure appears under
service and jwt alike, so a connection failure is never evidence about the credential.
Two banner lines that are not the problem¶
Runtime: X | Mode: N/A is a consequence of a failed connection under auth='service', not a
second fault. That path connects at import, so a host failure is fatal and the runtime never
constructs: the pod serves /health while the agent does not exist, which the platform reports as
unhealthy. Under auth='jwt' the same fault surfaces per request instead, because that path connects
lazily.
JWT Auth: ... | Issuer: ... and Authorization: disabled describe who may call the agent's HTTP
endpoint - inbound platform authentication. They say nothing about how the pod reaches Teradata, so
changing them cannot affect TDML_2000.
Deployment constraints¶
Four settings differ from the platform tutorial's, and each is load-bearing rather than a preference.
| Setting | Value | Why |
|---|---|---|
resources |
2Gi / 1000m |
512Mi does not hold teradataml + langchain + pandas + networkx |
replicas |
1 |
conversation memory is an in-process MemorySaver; a second replica answers with no history |
enable_memory |
False |
the platform's PostgreSQL checkpointer cannot be injected into the tdfs4ds graph, so True provisions a database nothing reads |
| turns | serialized | _with_domain mutates the module-global DATA_DOMAIN; concurrent turns in one pod corrupt each other. A lock in the generated code enforces this |
Session memory is lost on restart
Multi-turn context works for as long as the pod lives, keyed on the
session_id the caller sends. It does not survive a restart and is not
shared between replicas. Lifting that needs a checkpointer= passthrough in
tdfs4ds.agent.graph.
Tracing¶
The platform's own collector records each call to the agent as one span. From 0.3.2.0 the package also traces inside the turn with Opik:
config.yamlsetsTDFS4DS_TRACING: "opik", andrequirements.txtinstallsopik;- each turn becomes an Opik trace, with a span per graph node and per LLM or tool
call, grouped by the conversation's
session_id; - the Opik SDK reads the
OPIK_*connection settings the platform injects. tdfs4ds names only the project (TDFS4DS_OPIK_PROJECT_NAME, defaulttdfs4ds); - the Opik SDK's own analytics and Sentry reports stay off
(
TDFS4DS_OPIK_VENDOR_TELEMETRY: "false").
The tracer is a LangChain callback in the run config, never a decorator: the
entrypoint must stay a bare coroutine (see What gets generated).
To deploy without it, set tdfs4ds.TRACING = 'off' in the session before packaging.
Not yet verified on a deployed pod
The traces were checked locally, against a capture of the Opik API: traces,
spans, threads, and token usage. That the platform-injected OPIK_* settings
route them to the AI Studio Opik has not been observed on a pod yet.
Preflight checks¶
check_agent_deployment() runs every
check_configuration check first — a
configuration broken locally cannot become correct in a container — then adds:
| Check | Catches |
|---|---|
config_deployable |
a required runtime variable the deployment would have to supply and you have not set |
pip_resolvable |
the running version is not published, so the pod's pip install fails minutes later |
agent_runtime |
zipping without the platform library — the definition registers and the pod then fails at startup |
structured_output |
STRUCTURED_OUTPUT_METHOD unpinned, which costs a probe per pod and can fail outright behind a relaying gateway |
vector_backend |
a Chroma index in a container, rebuilt on every restart |
business_dictionary |
an empty dictionary — the agent deploys mute |
connection_env |
TDFS4DS_TD_* unset, so the pod cannot open or recover its connection |
Like check_configuration, it never raises and never prints a secret — only
variable names.
Verifying the deployed agent¶
check_deployed_agent() sends three turns: an opening question, a follow-up on
the same session_id, and the same follow-up on a fresh one.
| Check | Failing means |
|---|---|
reachable |
401/403 = expired JWT; 404 = wrong URL; 5xx = the pod started then failed — read GET /agents/{agent_id}/logs |
answers |
HTTP 200 with an empty body |
grounded |
the pod answered without reaching the feature store — compare its startup configuration block against this session's SCHEMA / DATA_DOMAIN |
session_memory |
session_id never reached the graph, or replicas > 1 |
session_isolation |
conversations are shared between callers — the thread id has degraded to a constant |
latency |
cold vs warm call times |
grounded is the check a generic smoke test cannot make: a pod whose Teradata
connection failed still answers fluently, about nothing. It asserts a real
catalog name appears in the response.
session_isolation is the security-relevant one. A shared conversation reads as
a working memory feature from outside, and leaks answers between users.
Related¶
fs-deploy-agentops— the skill that drives this workflow- Consumer agent — what is being deployed
- OpenAI-compatible API server — the self-hosted alternative