fs-deploy-agentops¶
Package and deploy the consumer agent to Teradata AI Studio's AgentOps platform — preflight, generate, zip, deploy, smoke-test.
name: "Feature Store: Deploy to AgentOps"
description: "Package and deploy the tdfs4ds consumer agent to Teradata AI Studio's AgentOps platform — preflight the configuration, generate agent.py/customer_agent.py/config.yaml/requirements.txt from the running session, build the deployable zip, and smoke-test the live agent. Invoke when the user wants the feature-store chatbot running as a hosted AI Studio service rather than a local Gradio app. SKIP when the user wants to run the agent locally — use fs-agent."
argument-hint: "[agent-directory][mcp-enabled: true|false]"
allowed-tools: "Read, Edit, Write, Bash, Glob, Grep"
Generate the deployment package that runs the tdfs4ds consumer agent as a hosted service on Teradata AI Studio's AgentOps platform, then verify the deployed agent actually works.
User context: $ARGUMENTS
Related skills (call before / after this one)¶
| Step | Skill | Why |
|---|---|---|
| Before | fs-setup |
The package is derived from a working session — connect and setup() first. |
| Before | fs-document |
The agent answers only from the business dictionary. An undocumented store deploys a mute agent, and the preflight fails on exactly this. |
| Before | fs-vector-store |
Prefer the Teradata backend for a pod: a Chroma index lives on the container filesystem and is rebuilt on every restart. |
| Before | fs-agent |
Run the agent locally first. If it cannot answer here, containerising it will not help. |
| After | fs-inspect |
If the deployed agent finds nothing, confirm what is actually registered. |
The platform contract¶
AgentOps deploys from a zip holding four files plus its own agent_runtime/ library:
| File | Owner |
|---|---|
agent.py |
generated — platform entry point |
customer_agent.py |
generated — agent logic |
config.yaml |
generated — non-secret settings, committed |
requirements.txt |
generated — pip dependencies |
agent_runtime/ |
the platform's SDK — generated by tdfs4ds when absent; a platform copy is never overwritten |
agent_runtime is on no package index — the PyPI names agentops and agent_runtime are unrelated third-party projects, and installing either produces a green build then an ImportError at startup. Never put them in requirements.txt.
By default (agent_runtime='auto') tdfs4ds generates a compatibility implementation of the two-symbol contract when the destination has none, so no scaffold from demo_agents/ is needed. If a platform-supplied copy is present it always wins — its host may rely on details beyond the public surface. Use agent_runtime='require' to insist on the platform's own.
One call (preferred)¶
report = tdfs4ds.deploy_to_agentops(
'./teradata_agent',
database='MY_FEATURE_STORE', # REQUIRED - stated, never inherited
databases_readonly=['LAB_STORE'], # optional, each validated
data_domain='CLAIMS',
token=user_token, auth='jwt', mcp=False,
progress=print)
Seven stages: validate target -> preflight -> publication gate -> package+zip -> register -> deploy
-> verify. A failing stage stops the pipeline (registering a definition whose preflight failed
only creates registry litter and hides the cause). dry_run=True stops after the zip and returns
the payload without touching the API — always run it first.
report['insights'] carries the pod-log findings, analysed whether or not the deploy succeeded.
Tracing (0.3.2.0). The package traces each turn with Opik: config.yaml sets
TDFS4DS_TRACING: "opik" and requirements.txt installs opik. Each turn gets a span per graph
node and per LLM/tool call, grouped by session_id. The Opik SDK reads the platform-injected
OPIK_* settings. Its own analytics and Sentry reports stay off (TDFS4DS_OPIK_VENDOR_TELEMETRY:
"false"). Set tdfs4ds.TRACING = 'off' before packaging to deploy without it. Not yet observed on a
real pod: say so if asked whether traces reach the AI Studio Opik.
database= is required. The feature store is a deployment target, not ambient state: without
this the pod silently inherits whatever the session had configured, which is invisible at the call
site. Model / embedding / Vector Store settings still come from the running session.
Deploy with mcp=False first. It removes Enterprise auth and the 132-tool context budget from
the run, so any failure is unambiguously about the agent. Only two intents are omitted
(DATA_QUERY, ALGORITHM); every other skill works.
Reading the pod log¶
report['insights'] (or tdfs4ds.analyze_pod_log(text)) matches known signatures:
| Signature | Means |
|---|---|
pip_unresolvable |
the version is not on the pod's index — the build never reached your code |
agent_runtime_missing |
the platform library is in neither the zip nor the base image |
no_credentials |
runtime_config did not arrive, or the wrong auth mode |
token_rejected |
the JWT expired, or the caller cannot log on |
schema_missing |
the named store is not readable by the identity the pod connects as |
structured_output |
the gateway relays response_format without enforcing it |
oom |
resources too small |
startup_ok |
positive — the agent reached its own configuration check |
An empty result is not a healthy start: look for startup_ok to confirm one.
Step-by-step (when you need the pieces)¶
import tdfs4ds
# 1. Preflight — in the session that already answers questions correctly.
# Folds in check_configuration(), then adds the deployment-specific checks.
tdfs4ds.check_agent_deployment(dest='./teradata_agent')
# 2. Inspect before writing anything (the repo's generate/write idiom).
result = tdfs4ds.generate_agentops_package(mcp_enabled=False)
print(result['files']['customer_agent.py'])
print(result['files']['config.yaml'])
# 3. Write into the scaffolded directory + build the zip.
result = tdfs4ds.write_agentops_package('./teradata_agent', mcp_enabled=False)
print(result['zip_path']) # -> ./teradata_agent.zip
print(result['runtime_config']) # -> goes in the deployment payload, NOT on disk
Then run notebook 02's upload / poll / deploy cells unchanged, using the generated payload:
Finally, verify the live agent:
Authentication: auth='service' vs auth='jwt'¶
Pick before generating; it changes the agent, not just a setting.
| Mode | When | How the pod connects |
|---|---|---|
service (default) |
You have a Teradata username/password for a service account | One long-lived context opened at import from TDFS4DS_TD_*. All callers read as that user. |
jwt |
The platform authenticates each caller with a token and there is no password | No connection at import. Each caller's auth_context.token opens the session, rebuilt when the caller changes. |
The pod's token is NOT
%gen_token's, and that distinction is the whole risk. AI Studio issues two tokens that look alike: the session/SSO token — which is what reaches the pod asauth_context.token— and%gen_token'suser_token, minted for a Vantage logon. Confirmed live: the first is refused by the database withError 8017 / SQLState 28000, the second is accepted, and they are different strings. A pod cannot call%gen_token, soauth='jwt'works only where the platform's own token is also accepted as a database logon — a Vantage-side trust setting for the token's issuer, not anything the deployment configures. Verify before deploying (§ below); where it is refused,auth='service'is the way in.
Under jwt, three consequences follow from the token model and are baked into the generated code: the bootstrap is lazy (no credential exists at import — tokens arrive per request); the rebuild is keyed on the token (tokens expire, and the next call brings a fresh one, so it self-heals); and the context is rebuilt per caller. That last is only safe because turns are already serialized — teradataml keeps one process-wide context, so interleaved turns would run under the wrong identity. connect() still runs once per process: catalog views belong to the store, not the caller.
Costs of jwt, state them to the user: every caller needs SELECT rights on the feature store (there is no service account behind it), and switching caller costs a reconnect. Only a fingerprint of the token is kept, never the token.
tdfs4ds.check_agent_deployment(auth='jwt')
result = tdfs4ds.write_agentops_package('./teradata_agent', auth='jwt')
Under jwt only TDFS4DS_TD_HOST is required at deploy time.
TDFS4DS_TD_HOST in a pod: use the IP address¶
Verified on a live AgentOps deployment: dbc does not resolve from inside the pod, and
neither did any qualified form - DNS for the database was unavailable there entirely. The fix was to
set TDFS4DS_TD_HOST to the database's IP address.
This matters because the notebook is not evidence. 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
works in one and fails in the other, and the failure carries no hint that a name was the problem:
[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 timing. Those two lines are 88 ms apart. TDML_2000 is identical for three
different faults, and the elapsed time separates them:
| 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 or firewall | a platform question, not a setting |
a round trip, SQLSTATE 28000 |
reached and refused | the credential (auth= mode, password, token audience) |
Since v0.3.1.184 the generated agent prints this diagnosis itself on the failure path - the resolved addresses or the resolver's error, the resolver's search domains, and a TCP probe of port 1025 when a name does resolve. Read it before changing anything.
Two consequences of using an IP, state both to the user: it pins the deployment to an address that
can change, so re-check it if a working pod later stops connecting; and it is unrelated to the auth
mode - the same host failure appears under service and jwt alike, so a connection failure is
never evidence about the credential.
Also: Runtime: X | Mode: N/A in the pod banner is a consequence of this under auth='service',
not a second problem. 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 jwt the identical fault appears per request instead, because that path
connects lazily.
And JWT Auth: ... | Issuer: ... / Authorization: disabled in that banner describe who may call
the agent's HTTP endpoint - inbound platform auth. They say nothing about the Teradata connection;
changing them cannot affect TDML_2000.
Where each setting goes — and why¶
The split is derived from tdfs4ds.config's own credential classification, never guessed:
| Destination | Contents | Rationale |
|---|---|---|
config.yaml |
ordinary settings (SCHEMA, DATA_DOMAIN, model names, AGENT_ANSWER_STYLE…) |
bundled in the zip and committed — must be safe to read |
runtime_config |
credentials, TDFS4DS_TD_*, TDFS4DS_VECTOR_STORE_AUTH_*, MCP_USERNAME, MCP_AUTH |
injected at deploy time; connection vars identify a deployment, so committing them pins the package to the environment it was built in |
| dropped | TDFS4DS_SERVE_*, and CHROMA_* on the Teradata backend |
the platform is the server; a Chroma path in a pod is misleading |
config.yaml lists the omitted variable names as comments, so whoever reads it can tell what the deployment must supply.
MCP_USERNAME and MCP_AUTH are named individually because neither is a secret by name
and neither carries a protected prefix. 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 (build_mcp_headers resolves Basic from it before the edition), so a committed file
could change how the pod authenticates. Under mcp=False both are dropped anyway, so the
default deployment is unaffected. They are deliberately not added to config._is_secret:
that is the package-wide authority on "is this a credential?", and neither is one -- "may
this be committed?" is a different question, which _DEPLOYMENT_VARS answers.
runtime_config is not a vault - do not describe it as one. Its guarantee is "not
committed to source control", which is weaker than confidential: the values are stored in
the deployment record and displayed in clear in the AgentOps UI to anyone who can view
the deployment. State this to the user, because a deploy effectively publishes those
credentials. The destination still matters -- git history is permanent and widely readable,
while a deployment record can be deleted and its credentials rotated -- but the difference is
durability and blast radius, not secrecy.
Deployment constraints — not preferences¶
The generated deployment_payload() differs from the tutorial's in four places, each load-bearing:
| 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 | graph._with_domain mutates the module-global DATA_DOMAIN; concurrent turns in one pod corrupt each other. Enforced by an asyncio.Lock in the generated code |
Consequence to state plainly to the user: session memory works for as long as the pod lives and is lost on restart. That is a known limit of this packaging, not a misconfiguration. Lifting it needs a checkpointer= passthrough in tdfs4ds/agent/graph.py.
Known blockers, in the order they bite¶
- The version must be installable from the pod's index. A local-only build imports fine here and fails
pip installthere. Thepip_resolvablecheck catches it; the fix is to publish, or vendor a wheel into the agent directory. STRUCTURED_OUTPUT_METHODunset. An in-cluster LiteLLM gateway commonly relaysresponse_formatwithout enforcing it, and the defaultjson_schemapath then dies on the first intent classification. Pin it. Do not guess the value:check_configuration()probes the endpoint and names it, andcheck_configuration(pin=True)records it in the.envstamped with the endpoint and date. Note that a pinned value travels into the pod'sconfig.yaml, so re-probe if the pod uses a different gateway.-
auth='jwt'where the environment allows it. The token arrives per request so expiry self-heals, the pod connects as the caller, and no durable password sits inruntime_config(which renders in clear in the deployment UI). But it is conditional: the platform'sauth_context.tokenmust be accepted by Vantage as a JWT logon, and a token minted for a platform API is not automatically a database credential — confirmed refused on at least one site. Settle it from the notebook before deploying, with the token the pod will see (OAUTH_ACCESS_TOKEN, not%gen_token's):import os, teradatasql teradatasql.connect(host='<db-ip>', logmech='JWT', logdata='token=' + os.environ['OAUTH_ACCESS_TOKEN']).close()Raw
teradatasql, nottdml.create_context: teradataml holds one process-wide context, so testing with it would replace the working connection in your own notebook.Error 8017there meansauth='jwt'is not deployable on that site. 4. Noagent_runtime/. The definition registers happily and the pod then fails at startup. 5. Empty business dictionary. The agent deploys and answers nothing.
Reading the smoke test¶
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 (re-run %gen_token); 404 = wrong URL; 5xx = the pod started then failed — read GET /agents/{agent_id}/logs |
grounded |
the pod answered without reaching the feature store — compare its startup check_configuration block against this session's SCHEMA / DATA_DOMAIN |
session_memory |
session_id never reached the graph as thread_id, or replicas > 1 |
session_isolation |
conversations are shared between callers — answers can leak between users. The thread id has degraded to a constant |
session_memory failing while session_isolation passes isolates the fault to session_id plumbing; both failing means the thread id is constant.
Do not¶
- Do not hand-edit the generated files. Regenerate, so the deployment keeps matching the session it was built from.
- Do not put credentials in
config.yaml— it is committed, permanently.runtime_configis the right destination, but tell the user it is readable in the deployment UI rather than implying it is secret. - Do not raise
replicasto scale. Scale with separate deployments until the graph accepts an external checkpointer. - Do not set
streaming=True.consumer_agent_streamis a synchronous generator; bridging it to the platform's SSE format is not a passthrough.