Give an agent a file to work on
The scripts p1-inputs.sh, p1-inputs.py and p1-inputs.ts are in full under Complete script.
Before you start
- Set
CONTEXT_BASE_URLto your deployment’s host, without/v1(for example,https://api.context.ai). - Set
CONTEXT_API_KEYto a personal Task API key. The default capabilities are enough. - Set
AGENT_IDto an agent id fromGET /v1/agents.
Client setup used by every step
#!/usr/bin/env bashset -euo pipefail: "${CONTEXT_BASE_URL:?Set CONTEXT_BASE_URL, for example https://api.context.ai}": "${CONTEXT_API_KEY:?Set CONTEXT_API_KEY to a personal Task API key}": "${AGENT_ID:?Set AGENT_ID to an id from GET /v1/agents}"api() { local path=$1; shift curl -sS --fail-with-body "$CONTEXT_BASE_URL/v1$path" -H "x-api-key: $CONTEXT_API_KEY" "$@"}fail() { echo "error: $*" >&2; exit 1; }# Opens the session's event stream; exec makes the background job's pid the curl process itself.stream() { exec curl -sS --fail-with-body -N "$CONTEXT_BASE_URL/v1/sessions/$1/events/stream" \ -H "x-api-key: $CONTEXT_API_KEY" "${@:2}"}# The JSON of every event received so far on a stream saved to a file. Ping frames carry no id and are skipped.events_in() { sed -n 's/^data: //p' "$1" | jq -c 'select(.id)'; }# Waits until the stream file holds a session.status_idle that ends the turn (not requires_action).wait_for_end_of_turn() { for _ in $(seq 1 300); do grep -q '^event: error' "$1" && fail "the stream ended with an error: $(grep -A2 '^event: error' "$1" | sed -n 's/^data: //p')" events_in "$1" | jq -se 'any(.[]; .type == "session.status_idle" and .stop_reason.type != "requires_action")' >/dev/null && return 0 sleep 1 done fail "the turn did not end within 300 seconds"}WORK=$(mktemp -d)import os
from anthropic import Anthropic
client = Anthropic( base_url=os.environ["CONTEXT_BASE_URL"], api_key=os.environ["CONTEXT_API_KEY"], max_retries=0,)agent_id = os.environ["AGENT_ID"]
def check(ok, message): if not ok: raise SystemExit(f"error: {message}")import Anthropic, { toFile } from "@anthropic-ai/sdk"
const client = new Anthropic({ baseURL: process.env.CONTEXT_BASE_URL, apiKey: process.env.CONTEXT_API_KEY, maxRetries: 0,})const agentId = process.env.AGENT_ID!
function check(ok: unknown, message: string): asserts ok { if (!ok) throw new Error(message)}-
Upload a file
Send a multipart request with one part named
filethat carries a filename. The file is stored in your Drive, your file storage in the Context app, and the response is its metadata.cURL printf 'city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n' > "$WORK/cities.csv"FILE=$(api /files -F "file=@$WORK/cities.csv;type=text/csv")FILE_ID=$(jq -r .id <<<"$FILE")[[ $(jq -r .filename <<<"$FILE") == "cities.csv" ]] || fail "unexpected upload: $FILE"echo "step 1: uploaded $FILE_ID cities.csv ($(jq -r .size_bytes <<<"$FILE") bytes)"Python csv = b"city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n"uploaded = client.beta.files.upload(file=("cities.csv", csv, "text/csv"))check(uploaded.filename == "cities.csv", f"unexpected upload {uploaded}")check(uploaded.size_bytes == len(csv), f"size {uploaded.size_bytes}, sent {len(csv)}")print(f"step 1: uploaded {uploaded.id} cities.csv ({uploaded.size_bytes} bytes)")TypeScript const csv = Buffer.from("city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n")const uploaded = await client.beta.files.upload({ file: await toFile(csv, "cities.csv", { type: "text/csv" }) })check(uploaded.filename === "cities.csv", `unexpected upload ${JSON.stringify(uploaded)}`)check(uploaded.size_bytes === csv.length, `size ${uploaded.size_bytes}, sent ${csv.length}`)console.log(`step 1: uploaded ${uploaded.id} cities.csv (${uploaded.size_bytes} bytes)`)Sample output:
step 1: uploaded 329409ba-f288-4b44-9c73-14a18150edee cities.csv (61 bytes) -
Create a session with the file attached
List the file in
resources. The session reports where the file is asmount_path, which isinputs/<filename>.cURL SESSION=$(api /sessions -H "content-type: application/json" -d "$(jq -n --arg agent "$AGENT_ID" --arg file "$FILE_ID" '{agent: $agent, environment_id: "env_default", title: "Cities report", resources: [{type: "file", file_id: $file}]}')")SESSION_ID=$(jq -r .id <<<"$SESSION")[[ $(jq -r '.resources[0].file_id' <<<"$SESSION") == "$FILE_ID" ]] || fail "file is not attached: $SESSION"MOUNT=$(jq -r '.resources[0].mount_path' <<<"$SESSION")[[ $MOUNT == "inputs/cities.csv" ]] || fail "unexpected mount_path: $MOUNT"echo "step 2: session $SESSION_ID has $FILE_ID at $MOUNT"Python session = client.beta.sessions.create(agent=agent_id,environment_id="env_default",title="Cities report",resources=[{"type": "file", "file_id": uploaded.id}],)resource = session.resources[0]check(resource.file_id == uploaded.id, f"file is not attached: {session.resources}")check(resource.mount_path == "inputs/cities.csv", f"unexpected mount_path {resource.mount_path}")print(f"step 2: session {session.id} has {uploaded.id} at {resource.mount_path}")TypeScript const session = await client.beta.sessions.create({agent: agentId,environment_id: "env_default",title: "Cities report",resources: [{ type: "file", file_id: uploaded.id }],})const resource = session.resources[0]check(resource?.type === "file" && resource.file_id === uploaded.id, `file is not attached: ${JSON.stringify(session.resources)}`)check(resource.mount_path === "inputs/cities.csv", `unexpected mount_path ${resource.mount_path}`)console.log(`step 2: session ${session.id} has ${uploaded.id} at ${resource.mount_path}`)Sample output:
step 2: session dd13d728-93e3-440a-874c-4efeb58c97e0 has 329409ba-f288-4b44-9c73-14a18150edee at inputs/cities.csv -
Ask the agent about the file
Open the stream first, then send the message. The turn is over at a
session.status_idlewhosestop_reason.typeis notrequires_action.cURL stream "$SESSION_ID" > "$WORK/stream.txt" &STREAM_PID=$!sleep 1 # open the stream before sendingkill -0 "$STREAM_PID" 2>/dev/null || fail "the stream did not open: $(cat "$WORK/stream.txt")"api "/sessions/$SESSION_ID/events" -H "content-type: application/json" -d '{"events": [{"type": "user.message","content": [{"type": "text", "text": "Read inputs/cities.csv and name the largest city."}]}]}' > /dev/nullwait_for_end_of_turn "$WORK/stream.txt"kill "$STREAM_PID"ANSWER=$(events_in "$WORK/stream.txt" | jq -r 'select(.type == "agent.message") | .content[] | select(.type == "text") | .text')[[ -n $ANSWER ]] || fail "no agent.message on the stream"echo "step 3: agent said: $ANSWER"Python stream = client.beta.sessions.events.stream(session.id) # open before sendingclient.beta.sessions.events.send(session.id,events=[{"type": "user.message", "content": [{"type": "text", "text": "Read inputs/cities.csv and name the largest city."}]}],)answer = ""for event in stream:if event.type == "agent.message":answer += "".join(block.text for block in event.content if block.type == "text")if event.type == "session.status_idle" and event.stop_reason.type != "requires_action":breakstream.close()check(answer, "no agent.message on the stream")print(f"step 3: agent said: {answer}")TypeScript const stream = await client.beta.sessions.events.stream(session.id) // open before sendingawait client.beta.sessions.events.send(session.id, {events: [{ type: "user.message", content: [{ type: "text", text: "Read inputs/cities.csv and name the largest city." }] }],})let answer = ""for await (const event of stream) {if (event.type === "agent.message") answer += event.content.map(b => (b.type === "text" ? b.text : "")).join("")if (event.type === "session.status_idle" && event.stop_reason.type !== "requires_action") break}stream.controller.abort()check(answer, "no agent.message on the stream")console.log(`step 3: agent said: ${answer}`)Sample output:
step 3: agent said: Tokyo is the largest city in the file, with 37.4 million people. -
Archive the session and delete the file
Archiving ends the session. Its status becomes
terminatedand its history stays readable. A file that a session still uses cannot be deleted until that session is archived.cURL STATUS=$(api "/sessions/$SESSION_ID/archive" -X POST | jq -r .status)[[ $STATUS == "terminated" ]] || fail "expected terminated, got $STATUS"# Allowed once no live session uses the file.DELETED=$(api "/files/$FILE_ID" -X DELETE)[[ $(jq -r .type <<<"$DELETED") == "file_deleted" ]] || fail "unexpected delete response: $DELETED"echo "step 4: session $STATUS, file $FILE_ID deleted"Python archived = client.beta.sessions.archive(session.id)check(archived.status == "terminated", f"expected terminated, got {archived.status}")deleted = client.beta.files.delete(uploaded.id) # allowed once no live session uses the filecheck(deleted.type == "file_deleted", f"unexpected delete response {deleted}")print(f"step 4: session {archived.status}, file {deleted.id} deleted")TypeScript const archived = await client.beta.sessions.archive(session.id)check(archived.status === "terminated", `expected terminated, got ${archived.status}`)const deleted = await client.beta.files.delete(uploaded.id) // allowed once no live session uses the filecheck(deleted.type === "file_deleted", `unexpected delete response ${JSON.stringify(deleted)}`)console.log(`step 4: session ${archived.status}, file ${deleted.id} deleted`)Sample output:
step 4: session terminated, file 329409ba-f288-4b44-9c73-14a18150edee deleted
If something goes wrong
| What you see | Why | What to do |
|---|---|---|
400 invalid_request_error on upload | The multipart part is not named file, has no filename, or there is more than one part. | Send exactly one part named file with a filename: a (name, bytes, type) tuple in Python, toFile(bytes, name) in TypeScript, or -F file=@path with cURL. |
403 permission_error on sessions.create | AGENT_ID is not an agent this key can run. An id that does not exist returns the same 403. | Pick an id from GET /v1/agents. |
404 not_found_error naming the file on sessions.create | The file id is not one uploaded through /v1/files that this key can read. | Use the id returned by the upload in step 1. |
400 invalid_request_error on the file delete | A session that is not archived still uses the file, or an archived session whose last turn is still running uses it. | Archive that session, then delete the file. |
Complete script
#!/usr/bin/env bashset -euo pipefail: "${CONTEXT_BASE_URL:?Set CONTEXT_BASE_URL, for example https://api.context.ai}": "${CONTEXT_API_KEY:?Set CONTEXT_API_KEY to a personal Task API key}": "${AGENT_ID:?Set AGENT_ID to an id from GET /v1/agents}"api() { local path=$1; shift curl -sS --fail-with-body "$CONTEXT_BASE_URL/v1$path" -H "x-api-key: $CONTEXT_API_KEY" "$@"}fail() { echo "error: $*" >&2; exit 1; }# Opens the session's event stream; exec makes the background job's pid the curl process itself.stream() { exec curl -sS --fail-with-body -N "$CONTEXT_BASE_URL/v1/sessions/$1/events/stream" \ -H "x-api-key: $CONTEXT_API_KEY" "${@:2}"}# The JSON of every event received so far on a stream saved to a file. Ping frames carry no id and are skipped.events_in() { sed -n 's/^data: //p' "$1" | jq -c 'select(.id)'; }# Waits until the stream file holds a session.status_idle that ends the turn (not requires_action).wait_for_end_of_turn() { for _ in $(seq 1 300); do grep -q '^event: error' "$1" && fail "the stream ended with an error: $(grep -A2 '^event: error' "$1" | sed -n 's/^data: //p')" events_in "$1" | jq -se 'any(.[]; .type == "session.status_idle" and .stop_reason.type != "requires_action")' >/dev/null && return 0 sleep 1 done fail "the turn did not end within 300 seconds"}WORK=$(mktemp -d)
# @step 1 Upload a fileprintf 'city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n' > "$WORK/cities.csv"FILE=$(api /files -F "file=@$WORK/cities.csv;type=text/csv")FILE_ID=$(jq -r .id <<<"$FILE")[[ $(jq -r .filename <<<"$FILE") == "cities.csv" ]] || fail "unexpected upload: $FILE"echo "step 1: uploaded $FILE_ID cities.csv ($(jq -r .size_bytes <<<"$FILE") bytes)"
# @step 2 Create a session with the file attachedSESSION=$(api /sessions -H "content-type: application/json" -d "$(jq -n --arg agent "$AGENT_ID" --arg file "$FILE_ID" '{ agent: $agent, environment_id: "env_default", title: "Cities report", resources: [{type: "file", file_id: $file}]}')")SESSION_ID=$(jq -r .id <<<"$SESSION")[[ $(jq -r '.resources[0].file_id' <<<"$SESSION") == "$FILE_ID" ]] || fail "file is not attached: $SESSION"MOUNT=$(jq -r '.resources[0].mount_path' <<<"$SESSION")[[ $MOUNT == "inputs/cities.csv" ]] || fail "unexpected mount_path: $MOUNT"echo "step 2: session $SESSION_ID has $FILE_ID at $MOUNT"
# @step 3 Ask the agent about the filestream "$SESSION_ID" > "$WORK/stream.txt" &STREAM_PID=$!sleep 1 # open the stream before sendingkill -0 "$STREAM_PID" 2>/dev/null || fail "the stream did not open: $(cat "$WORK/stream.txt")"api "/sessions/$SESSION_ID/events" -H "content-type: application/json" -d '{"events": [{"type": "user.message", "content": [{"type": "text", "text": "Read inputs/cities.csv and name the largest city."}]}]}' > /dev/nullwait_for_end_of_turn "$WORK/stream.txt"kill "$STREAM_PID"ANSWER=$(events_in "$WORK/stream.txt" | jq -r 'select(.type == "agent.message") | .content[] | select(.type == "text") | .text')[[ -n $ANSWER ]] || fail "no agent.message on the stream"echo "step 3: agent said: $ANSWER"
# @step 4 Archive the session and delete the fileSTATUS=$(api "/sessions/$SESSION_ID/archive" -X POST | jq -r .status)[[ $STATUS == "terminated" ]] || fail "expected terminated, got $STATUS"# Allowed once no live session uses the file.DELETED=$(api "/files/$FILE_ID" -X DELETE)[[ $(jq -r .type <<<"$DELETED") == "file_deleted" ]] || fail "unexpected delete response: $DELETED"echo "step 4: session $STATUS, file $FILE_ID deleted"# @endimport os
from anthropic import Anthropic
client = Anthropic( base_url=os.environ["CONTEXT_BASE_URL"], api_key=os.environ["CONTEXT_API_KEY"], max_retries=0,)agent_id = os.environ["AGENT_ID"]
def check(ok, message): if not ok: raise SystemExit(f"error: {message}")
# @step 1 Upload a filecsv = b"city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n"uploaded = client.beta.files.upload(file=("cities.csv", csv, "text/csv"))check(uploaded.filename == "cities.csv", f"unexpected upload {uploaded}")check(uploaded.size_bytes == len(csv), f"size {uploaded.size_bytes}, sent {len(csv)}")print(f"step 1: uploaded {uploaded.id} cities.csv ({uploaded.size_bytes} bytes)")
# @step 2 Create a session with the file attachedsession = client.beta.sessions.create( agent=agent_id, environment_id="env_default", title="Cities report", resources=[{"type": "file", "file_id": uploaded.id}],)resource = session.resources[0]check(resource.file_id == uploaded.id, f"file is not attached: {session.resources}")check(resource.mount_path == "inputs/cities.csv", f"unexpected mount_path {resource.mount_path}")print(f"step 2: session {session.id} has {uploaded.id} at {resource.mount_path}")
# @step 3 Ask the agent about the filestream = client.beta.sessions.events.stream(session.id) # open before sendingclient.beta.sessions.events.send( session.id, events=[{"type": "user.message", "content": [{"type": "text", "text": "Read inputs/cities.csv and name the largest city."}]}],)answer = ""for event in stream: if event.type == "agent.message": answer += "".join(block.text for block in event.content if block.type == "text") if event.type == "session.status_idle" and event.stop_reason.type != "requires_action": breakstream.close()check(answer, "no agent.message on the stream")print(f"step 3: agent said: {answer}")
# @step 4 Archive the session and delete the filearchived = client.beta.sessions.archive(session.id)check(archived.status == "terminated", f"expected terminated, got {archived.status}")deleted = client.beta.files.delete(uploaded.id) # allowed once no live session uses the filecheck(deleted.type == "file_deleted", f"unexpected delete response {deleted}")print(f"step 4: session {archived.status}, file {deleted.id} deleted")# @endimport Anthropic, { toFile } from "@anthropic-ai/sdk"
const client = new Anthropic({ baseURL: process.env.CONTEXT_BASE_URL, apiKey: process.env.CONTEXT_API_KEY, maxRetries: 0,})const agentId = process.env.AGENT_ID!
function check(ok: unknown, message: string): asserts ok { if (!ok) throw new Error(message)}
// @step 1 Upload a fileconst csv = Buffer.from("city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n")const uploaded = await client.beta.files.upload({ file: await toFile(csv, "cities.csv", { type: "text/csv" }) })check(uploaded.filename === "cities.csv", `unexpected upload ${JSON.stringify(uploaded)}`)check(uploaded.size_bytes === csv.length, `size ${uploaded.size_bytes}, sent ${csv.length}`)console.log(`step 1: uploaded ${uploaded.id} cities.csv (${uploaded.size_bytes} bytes)`)
// @step 2 Create a session with the file attachedconst session = await client.beta.sessions.create({ agent: agentId, environment_id: "env_default", title: "Cities report", resources: [{ type: "file", file_id: uploaded.id }],})const resource = session.resources[0]check(resource?.type === "file" && resource.file_id === uploaded.id, `file is not attached: ${JSON.stringify(session.resources)}`)check(resource.mount_path === "inputs/cities.csv", `unexpected mount_path ${resource.mount_path}`)console.log(`step 2: session ${session.id} has ${uploaded.id} at ${resource.mount_path}`)
// @step 3 Ask the agent about the fileconst stream = await client.beta.sessions.events.stream(session.id) // open before sendingawait client.beta.sessions.events.send(session.id, { events: [{ type: "user.message", content: [{ type: "text", text: "Read inputs/cities.csv and name the largest city." }] }],})let answer = ""for await (const event of stream) { if (event.type === "agent.message") answer += event.content.map(b => (b.type === "text" ? b.text : "")).join("") if (event.type === "session.status_idle" && event.stop_reason.type !== "requires_action") break}stream.controller.abort()check(answer, "no agent.message on the stream")console.log(`step 3: agent said: ${answer}`)
// @step 4 Archive the session and delete the fileconst archived = await client.beta.sessions.archive(session.id)check(archived.status === "terminated", `expected terminated, got ${archived.status}`)const deleted = await client.beta.files.delete(uploaded.id) // allowed once no live session uses the filecheck(deleted.type === "file_deleted", `unexpected delete response ${JSON.stringify(deleted)}`)console.log(`step 4: session ${archived.status}, file ${deleted.id} deleted`)// @end