Skip to content

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:

payload = tdfs4ds.deployment_payload(agent_definition_id, result['runtime_config'])

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.yaml sets TDFS4DS_TRACING: "opik", and requirements.txt installs opik;
  • 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, default tdfs4ds);
  • 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.