fix: create corpus via REST API directly, bypassing SDK backend_config bug

SDK 1.153.1 always defaults to Spanner when backend_config is omitted,
and crashes when it's provided. Use REST POST directly with
vectorDbConfig.ragManagedDb set to force serverless.
This commit is contained in:
chrischristiansen-glitch 2026-05-24 19:11:25 +02:00
parent 2beb6a359d
commit a2790be421

View File

@ -3,13 +3,14 @@
setup_corpus.py Create a Vertex AI RAG Engine corpus and import documents. setup_corpus.py Create a Vertex AI RAG Engine corpus and import documents.
Project: propane-will-491900-m5 Project: propane-will-491900-m5
Serverless mode is controlled via the project-level ragEngineConfig PATCH SDK 1.153.1 has a bug where backend_config crashes when provided,
(ensure_serverless_engine_config). Passing backend_config to create_corpus and defaults to Spanner when omitted. We bypass it by calling the
triggers a SDK 1.153.1 bug and must be omitted. REST API directly for corpus creation, then use the SDK for everything else.
""" """
import os import os
import json import json
import time
import subprocess import subprocess
import vertexai import vertexai
@ -25,11 +26,16 @@ GCS_SOURCE = os.environ.get(
) )
def get_token() -> str:
return subprocess.check_output(
["gcloud", "auth", "print-access-token"], text=True
).strip()
def ensure_serverless_engine_config() -> None: def ensure_serverless_engine_config() -> None:
""" """
Pre-flight: set project-level RAG Engine Config to basic (serverless) tier. Set project-level RAG Engine Config to basic (serverless) tier.
This is what actually controls the backend backend_config in create_corpus Polls the operation until done.
is intentionally omitted due to SDK bug in 1.153.1.
""" """
print("Ensuring RAG Engine Config is set to serverless (basic) tier...") print("Ensuring RAG Engine Config is set to serverless (basic) tier...")
endpoint = ( endpoint = (
@ -37,51 +43,116 @@ def ensure_serverless_engine_config() -> None:
f"/projects/{PROJECT_ID}/locations/{LOCATION}/ragEngineConfig" f"/projects/{PROJECT_ID}/locations/{LOCATION}/ragEngineConfig"
) )
payload = json.dumps({"ragManagedDbConfig": {"basic": {}}}) payload = json.dumps({"ragManagedDbConfig": {"basic": {}}})
token = get_token()
try:
token = subprocess.check_output(
["gcloud", "auth", "print-access-token"], text=True
).strip()
except subprocess.CalledProcessError:
print(" ⚠️ Could not get gcloud token — skipping pre-flight.")
return
result = subprocess.run( result = subprocess.run(
[ ["curl", "-s", "-X", "PATCH",
"curl", "-s", "-X", "PATCH", "-H", f"Authorization: Bearer {token}",
"-H", f"Authorization: Bearer {token}", "-H", "Content-Type: application/json",
"-H", "Content-Type: application/json", endpoint, "-d", payload],
endpoint, "-d", payload,
],
capture_output=True, text=True, capture_output=True, text=True,
) )
resp = json.loads(result.stdout)
resp = result.stdout if "error" in resp:
if '"error"' in resp: err = resp["error"]
err = json.loads(resp).get("error", {}) print(f" ⚠️ Engine config warning ({err.get('code')}): {err.get('message')} — continuing.")
code, msg = err.get("code"), err.get("message", "") return
if code == 400 and "already" in msg.lower():
print(f" ✓ Already configured: {msg}") op_name = resp.get("name", "")
if "/operations/" in op_name and not resp.get("done"):
print(f" Polling operation {op_name.split('/')[-1]}...")
op_url = f"https://{LOCATION}-aiplatform.googleapis.com/v1beta1/{op_name}"
for _ in range(20):
time.sleep(3)
token = get_token()
r = subprocess.run(
["curl", "-s", "-H", f"Authorization: Bearer {token}", op_url],
capture_output=True, text=True,
)
op = json.loads(r.stdout)
if op.get("done"):
break
else: else:
print(f" ⚠️ Engine config warning ({code}): {msg} — continuing anyway.") print(" ⚠️ Operation timed out — continuing anyway.")
else: return
print(" ✓ RAG Engine Config set to basic tier.")
print(" ✓ RAG Engine Config set to basic tier.")
def create_corpus_rest() -> str:
"""
Create corpus via REST API directly, bypassing SDK backend_config bug.
Returns the corpus resource name.
"""
url = (
f"https://{LOCATION}-aiplatform.googleapis.com/v1beta1"
f"/projects/{PROJECT_ID}/locations/{LOCATION}/ragCorpora"
)
payload = json.dumps({
"displayName": CORPUS_DISPLAY_NAME,
"ragEmbeddingModelConfig": {
"vertexPredictionEndpoint": {
"model": "publishers/google/models/text-embedding-004"
}
},
"vectorDbConfig": {
"ragManagedDb": {}
}
})
token = get_token()
result = subprocess.run(
["curl", "-s", "-X", "POST",
"-H", f"Authorization: Bearer {token}",
"-H", "Content-Type: application/json",
url, "-d", payload],
capture_output=True, text=True,
)
resp = json.loads(result.stdout)
if "error" in resp:
raise RuntimeError(f"Failed to create corpus: {resp['error']}")
# REST returns a long-running operation
op_name = resp.get("name", "")
if "/operations/" not in op_name:
raise RuntimeError(f"Unexpected response: {resp}")
print(f" Polling corpus creation operation...")
op_url = f"https://{LOCATION}-aiplatform.googleapis.com/v1beta1/{op_name}"
for _ in range(40):
time.sleep(5)
token = get_token()
r = subprocess.run(
["curl", "-s", "-H", f"Authorization: Bearer {token}", op_url],
capture_output=True, text=True,
)
op = json.loads(r.stdout)
if op.get("done"):
if "error" in op:
raise RuntimeError(f"Corpus creation failed: {op['error']}")
corpus_name = op["response"]["name"]
return corpus_name
raise RuntimeError("Corpus creation operation timed out.")
def get_or_create_corpus() -> RagCorpus: def get_or_create_corpus() -> RagCorpus:
"""Return existing corpus by display name, or create a new one.""" """Return existing corpus by display name, or create a new serverless one."""
for c in rag.list_corpora(): for c in rag.list_corpora():
if c.display_name == CORPUS_DISPLAY_NAME: if c.display_name == CORPUS_DISPLAY_NAME:
print(f"Corpus '{CORPUS_DISPLAY_NAME}' already exists: {c.name}") print(f"Corpus '{CORPUS_DISPLAY_NAME}' already exists: {c.name}")
return c return c
print(f"Creating corpus '{CORPUS_DISPLAY_NAME}' in {LOCATION}...") print(f"Creating corpus '{CORPUS_DISPLAY_NAME}' in {LOCATION} via REST...")
# NOTE: backend_config is intentionally omitted. corpus_name = create_corpus_rest()
# SDK 1.153.1 has a bug where passing any backend_config crashes. print(f"✓ Corpus created: {corpus_name}")
# Serverless mode is already guaranteed by ensure_serverless_engine_config().
corpus = rag.create_corpus(display_name=CORPUS_DISPLAY_NAME) # Re-fetch via SDK so we get a proper RagCorpus object
print(f"✓ Corpus created: {corpus.name}") for c in rag.list_corpora():
return corpus if c.name == corpus_name:
return c
raise RuntimeError(f"Corpus created but not found in list: {corpus_name}")
def import_documents(corpus: RagCorpus) -> None: def import_documents(corpus: RagCorpus) -> None: