Skip to content

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

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; omitted = none (0.3.6.9)
    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.

Find, call and remove a deployed agent by name

url = tdfs4ds.agent_endpoint("tdfs4ds-propensity-demo", token=user_token)          # base URL
inv = tdfs4ds.agent_endpoint("tdfs4ds-propensity-demo", invoke=True, token=user_token)  # POST here

tdfs4ds.remove_agent("tdfs4ds-propensity-demo", token=user_token, dry_run=True)     # what would go
tdfs4ds.remove_agent("tdfs4ds-propensity-demo", token=user_token)   # deployment, then definition
  • The agent is named by deployment name, UUID, or the short id the Playground shows (85f752e). A name reused by a terminated deployment resolves to the live one; several live matches are refused with the candidates listed.
  • GET /agents lists the whole workspace, so a name can be another user's agent: remove_agent refuses a deployment whose created_by is not the token's user unless others=True.
  • The definition is deleted too (delete_definition=True) unless another live deployment uses it.
  • A name can be deployed only once: before re-deploying under the same name, remove the earlier deployment (tdfs4ds.agentops.find_agent(name) raises LookupError when there is none).

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:

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

Finally, verify the live agent:

tdfs4ds.check_deployed_agent(agent_url, token=user_token)

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 as auth_context.token — and %gen_token's user_token, minted for a Vantage logon. Confirmed live: the first is refused by the database with Error 8017 / SQLState 28000, the second is accepted, and they are different strings. A pod cannot call %gen_token, so auth='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

  1. The version must be installable from the pod's index. A local-only build imports fine here and fails pip install there. The pip_resolvable check catches it; the fix is to publish, or vendor a wheel into the agent directory.
  2. STRUCTURED_OUTPUT_METHOD unset. An in-cluster LiteLLM gateway commonly relays response_format without enforcing it, and the default json_schema path then dies on the first intent classification. Pin it. Do not guess the value: check_configuration() probes the endpoint and names it, and check_configuration(pin=True) records it in the .env stamped with the endpoint and date. Note that a pinned value travels into the pod's config.yaml, so re-probe if the pod uses a different gateway.
  3. 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 in runtime_config (which renders in clear in the deployment UI). But it is conditional: the platform's auth_context.token must 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, not tdml.create_context: teradataml holds one process-wide context, so testing with it would replace the working connection in your own notebook. Error 8017 there means auth='jwt' is not deployable on that site. 4. No agent_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_config is the right destination, but tell the user it is readable in the deployment UI rather than implying it is secret.
  • Do not raise replicas to scale. Scale with separate deployments until the graph accepts an external checkpointer.
  • Do not set streaming=True. consumer_agent_stream is a synchronous generator; bridging it to the platform's SSE format is not a passthrough.