Skip to content

Commit 860c322

Browse files
r3v5claudefranciscojavierarceo
authored
feat(pgvector): migrate from psycopg2 to asyncpg (#5763)
# What does this PR do? <!-- Provide a short summary of what this PR does and why. Link to relevant issues if applicable. --> **What:** Replaces `psycopg2 `(synchronous) with `asyncpg` (async) in the PGVector vector store provider. **Why:** psycopg2 blocks the event loop on every database call. In an async server (uvicorn/FastAPI), this freezes ALL request handling during DB operations. With `asyncpg`, queries yield control via coroutines, allowing concurrent non-blocking PGVector I/O operations. <!-- If resolving an issue, uncomment and update the line below --> Closes #5753 🤖 Co-Authored with [Claude Code](https://claude.com/claude-code) ## Test Plan <!-- Describe the tests you ran to verify your changes with result summaries. *Provide clear instructions so the plan can be easily re-executed.* --> ### I created this bash script for testing: ``` #!/usr/bin/env bash # End-to-end test: PGVector file search via curl against OGX server. # Tests vector, keyword, hybrid search modes and Response API with file_search tool. # # This script manages the OGX server lifecycle: kills any existing server, # clears state, starts fresh, runs tests, then cleans up. # # Required env vars: # OPENAI_API_KEY - for Response API (chat completion via OpenAI) # # Optional env vars: # PGVECTOR_HOST - default: localhost # PGVECTOR_PORT - default: 5432 # PGVECTOR_DB - default: testvectordb # PGVECTOR_USER - default: user # PGVECTOR_PASSWORD - default: password # OGX_BASE_URL - default: http://localhost:8321/v1 # OGX_MODEL - default: openai/gpt-5.5 # PDF_PATH - default: /Users/ianmiller/Downloads/invoicesample.pdf # # Usage: # OPENAI_API_KEY=<YOUR_API_KEY> bash scripts/test_pgvector_file_search.sh set -uo pipefail BASE_URL="${OGX_BASE_URL:-http://localhost:8321/v1}" PDF_PATH="${PDF_PATH:-/Users/ianmiller/Downloads/invoicesample.pdf}" MODEL="${OGX_MODEL:-openai/gpt-5.5}" EMBEDDING_MODEL="sentence-transformers/nomic-ai/nomic-embed-text-v1.5" SERVER_LOG="/tmp/ogx-server-test.log" export PGVECTOR_DB="${PGVECTOR_DB:-testvectordb}" export PGVECTOR_HOST="${PGVECTOR_HOST:-localhost}" export PGVECTOR_PORT="${PGVECTOR_PORT:-5432}" export PGVECTOR_USER="${PGVECTOR_USER:-user}" export PGVECTOR_PASSWORD="${PGVECTOR_PASSWORD:-password}" PASS=0 FAIL=0 step() { echo "" echo "============================================================" echo " $1" echo "============================================================" } check_result() { local name="$1" local condition="$2" if [ "$condition" = "true" ]; then echo " -> $name: PASS" PASS=$((PASS + 1)) else echo " -> $name: FAIL" FAIL=$((FAIL + 1)) fi } json_get() { python3 -c "import sys,json; print(json.load(sys.stdin)$1)" 2>/dev/null } # ------------------------------------------------------------------ # Step A: Clean up via existing server (if running) # ------------------------------------------------------------------ step "Cleanup: removing ALL vector stores and files via API" if curl -s "$BASE_URL/models" 2>/dev/null | grep -q "data" 2>/dev/null; then echo " Existing server found, cleaning up..." # Delete ALL vector stores (not just pgvector-test — clean everything) curl -s "$BASE_URL/vector_stores" -H "Authorization: Bearer fake" 2>/dev/null | python3 -c " import sys, json data = json.load(sys.stdin) for vs in data.get('data', []): print(vs['id'], vs.get('name', 'unnamed')) " 2>/dev/null | while read -r vs_id vs_name; do echo " Deleting vector store: $vs_id ($vs_name)" curl -s -X DELETE "$BASE_URL/vector_stores/$vs_id" -H "Authorization: Bearer fake" > /dev/null done # Delete ALL files curl -s "$BASE_URL/files" -H "Authorization: Bearer fake" 2>/dev/null | python3 -c " import sys, json data = json.load(sys.stdin) for f in data.get('data', []): print(f['id'], f.get('filename', 'unnamed')) " 2>/dev/null | while read -r file_id file_name; do echo " Deleting file: $file_id ($file_name)" curl -s -X DELETE "$BASE_URL/files/$file_id" -H "Authorization: Bearer fake" > /dev/null done echo " API cleanup complete" else echo " No existing server running, skipping API cleanup" fi # ------------------------------------------------------------------ # Step B: Kill server, clear local state, start fresh # ------------------------------------------------------------------ step "Starting fresh OGX server" echo " Killing server..." pkill -f "ogx run" 2>/dev/null || true sleep 2 echo " Clearing local distribution state..." rm -f ~/.llama/distributions/starter/kvstore.db rm -f ~/.llama/distributions/starter/sql_store.db echo " Starting server (log: $SERVER_LOG)..." uv run ogx run run-pgvector-test.yaml > "$SERVER_LOG" 2>&1 & SERVER_PID=$! echo " Server PID: $SERVER_PID" echo " Waiting for server..." SERVER_READY=false for i in $(seq 1 120); do if curl -s "$BASE_URL/models" 2>/dev/null | grep -q "data" 2>/dev/null; then echo " Server ready after ${i}s" SERVER_READY=true break fi sleep 1 done if [ "$SERVER_READY" = "false" ]; then echo " ERROR: Server failed to start. Last 20 lines of log:" tail -20 "$SERVER_LOG" kill $SERVER_PID 2>/dev/null || true exit 1 fi echo " Server running. Will stay running after tests complete." # ------------------------------------------------------------------ # Step 1: Upload PDF file # ------------------------------------------------------------------ step "Step 1: Upload PDF file" if [ ! -f "$PDF_PATH" ]; then echo " ERROR: PDF not found at $PDF_PATH" exit 1 fi UPLOAD_RESP=$(curl -s "$BASE_URL/files" \ -H "Authorization: Bearer fake" \ -F "file=@$PDF_PATH" \ -F "purpose=assistants") FILE_ID=$(echo "$UPLOAD_RESP" | json_get "['id']") FILE_NAME=$(echo "$UPLOAD_RESP" | json_get "['filename']") FILE_SIZE=$(echo "$UPLOAD_RESP" | json_get "['bytes']") if [ -z "$FILE_ID" ]; then echo " ERROR: File upload failed" echo " Response: $UPLOAD_RESP" exit 1 fi echo " File uploaded: $FILE_ID" echo " Filename: $FILE_NAME" echo " Size: $FILE_SIZE bytes" # ------------------------------------------------------------------ # Step 2: Create vector store # ------------------------------------------------------------------ step "Step 2: Create vector store" VS_RESP=$(curl -s "$BASE_URL/vector_stores" \ -H "Authorization: Bearer fake" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"pgvector-test-invoice\", \"embedding_model\": \"$EMBEDDING_MODEL\", \"embedding_dimension\": 768 }") VS_ID=$(echo "$VS_RESP" | json_get "['id']") VS_STATUS=$(echo "$VS_RESP" | json_get "['status']") if [ -z "$VS_ID" ]; then echo " ERROR: Vector store creation failed" echo " Response: $VS_RESP" exit 1 fi echo " Vector store created: $VS_ID" echo " Status: $VS_STATUS" # ------------------------------------------------------------------ # Step 3: Attach file to vector store # ------------------------------------------------------------------ step "Step 3: Attach file to vector store" ATTACH_RESP=$(curl -s "$BASE_URL/vector_stores/$VS_ID/files" \ -H "Authorization: Bearer fake" \ -H "Content-Type: application/json" \ -d "{\"file_id\": \"$FILE_ID\"}") ATTACH_STATUS=$(echo "$ATTACH_RESP" | json_get "['status']") echo " Attachment status: $ATTACH_STATUS" RETRIES=0 while [ "$ATTACH_STATUS" = "in_progress" ] && [ $RETRIES -lt 60 ]; do sleep 1 RETRIES=$((RETRIES + 1)) ATTACH_RESP=$(curl -s "$BASE_URL/vector_stores/$VS_ID/files/$FILE_ID" \ -H "Authorization: Bearer fake") ATTACH_STATUS=$(echo "$ATTACH_RESP" | json_get "['status']") if [ $((RETRIES % 5)) -eq 0 ]; then echo " Still processing... (${RETRIES}s)" fi done echo " Final status: $ATTACH_STATUS" if [ "$ATTACH_STATUS" != "completed" ]; then echo " ERROR: File attachment failed" echo " Server log tail:" tail -20 "$SERVER_LOG" exit 1 fi # ------------------------------------------------------------------ # Step 4: Vector Store Search — mode=vector # ------------------------------------------------------------------ step "Step 4: Vector Store Search — mode=vector" VECTOR_RESP=$(curl -s "$BASE_URL/vector_stores/$VS_ID/search" \ -H "Authorization: Bearer fake" \ -H "Content-Type: application/json" \ -d '{ "query": "What invoice number belongs to Denny Gunawan?", "max_num_results": 5, "search_mode": "vector" }') VECTOR_COUNT=$(echo "$VECTOR_RESP" | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('data',[])))" 2>/dev/null || echo "0") echo " Results count: $VECTOR_COUNT" echo "$VECTOR_RESP" | python3 -c " import sys, json data = json.load(sys.stdin) for i, r in enumerate(data.get('data', [])): score = r.get('score', 0) text = r.get('content', [{}])[0].get('text', 'N/A')[:120] print(f' [{i+1}] score={score:.4f} | {text}...') " 2>/dev/null check_result "vector_search" "$([ "$VECTOR_COUNT" -gt 0 ] && echo true || echo false)" # ------------------------------------------------------------------ # Step 5: Vector Store Search — mode=keyword # ------------------------------------------------------------------ step "Step 5: Vector Store Search — mode=keyword" KEYWORD_RESP=$(curl -s "$BASE_URL/vector_stores/$VS_ID/search" \ -H "Authorization: Bearer fake" \ -H "Content-Type: application/json" \ -d '{ "query": "invoice apple total", "max_num_results": 5, "search_mode": "keyword" }') KEYWORD_COUNT=$(echo "$KEYWORD_RESP" | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('data',[])))" 2>/dev/null || echo "0") echo " Results count: $KEYWORD_COUNT" echo "$KEYWORD_RESP" | python3 -c " import sys, json data = json.load(sys.stdin) for i, r in enumerate(data.get('data', [])): score = r.get('score', 0) text = r.get('content', [{}])[0].get('text', 'N/A')[:120] print(f' [{i+1}] score={score:.4f} | {text}...') " 2>/dev/null check_result "keyword_search" "$([ "$KEYWORD_COUNT" -gt 0 ] && echo true || echo false)" # ------------------------------------------------------------------ # Step 6: Vector Store Search — mode=hybrid # ------------------------------------------------------------------ step "Step 6: Vector Store Search — mode=hybrid" HYBRID_RESP=$(curl -s "$BASE_URL/vector_stores/$VS_ID/search" \ -H "Authorization: Bearer fake" \ -H "Content-Type: application/json" \ -d '{ "query": "What invoice number belongs to Denny Gunawan?", "max_num_results": 5, "search_mode": "hybrid", "ranking_options": {"ranker": "rrf"} }') HYBRID_COUNT=$(echo "$HYBRID_RESP" | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('data',[])))" 2>/dev/null || echo "0") echo " Results count: $HYBRID_COUNT" echo "$HYBRID_RESP" | python3 -c " import sys, json data = json.load(sys.stdin) for i, r in enumerate(data.get('data', [])): score = r.get('score', 0) text = r.get('content', [{}])[0].get('text', 'N/A')[:120] print(f' [{i+1}] score={score:.4f} | {text}...') " 2>/dev/null check_result "hybrid_search" "$([ "$HYBRID_COUNT" -gt 0 ] && echo true || echo false)" # ------------------------------------------------------------------ # Step 7: Response API with file_search tool # ------------------------------------------------------------------ step "Step 7: Response API with file_search tool" echo " NOTE: Responses API uses default_search_mode from server config." echo " To change mode, update run-pgvector-test.yaml and restart server." RESPONSE_RESP=$(curl -s "$BASE_URL/responses" \ -H "Authorization: Bearer fake" \ -H "Content-Type: application/json" \ -d "{ \"model\": \"$MODEL\", \"input\": \"What invoice number belongs to Denny Gunawan?\", \"tools\": [ { \"type\": \"file_search\", \"vector_store_ids\": [\"$VS_ID\"] } ], \"stream\": false, \"include\": [\"file_search_call.results\"] }") echo "" echo " --- Raw JSON Response ---" echo "$RESPONSE_RESP" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE_RESP" echo " --- End Raw JSON ---" echo "" RESP_STATUS=$(echo "$RESPONSE_RESP" | json_get ".get('status','failed')" || echo "failed") RESP_ID=$(echo "$RESPONSE_RESP" | json_get ".get('id','N/A')" || echo "N/A") echo " Response ID: $RESP_ID" echo " Status: $RESP_STATUS" echo "$RESPONSE_RESP" | python3 -c " import sys, json data = json.load(sys.stdin) for item in data.get('output', []): if item.get('type') == 'file_search_call': print(f\" File search call ID: {item.get('id')}\") print(f\" Status: {item.get('status')}\") results = item.get('results', []) if results: print(f' Search results: {len(results)}') for j, r in enumerate(results[:3]): text = (r.get('text') or 'N/A')[:100] score = r.get('score', 0) file_id = r.get('file_id', 'N/A') print(f' [{j+1}] score={score:.4f} file={file_id} | {text}...') elif item.get('type') == 'message': for content in item.get('content', []): if content.get('type') == 'output_text': print(f\"\") print(f\" LLM Response:\") print(f\" {content['text'][:500]}\") " 2>/dev/null check_result "response_file_search" "$([ "$RESP_STATUS" = "completed" ] && echo true || echo false)" # ------------------------------------------------------------------ # Results Summary # ------------------------------------------------------------------ step "RESULTS SUMMARY" echo " Passed: $PASS" echo " Failed: $FAIL" if [ $FAIL -eq 0 ]; then echo "" echo " All tests passed!" exit 0 else echo "" echo " Some tests failed!" echo " Server log: $SERVER_LOG" exit 1 fi ``` ### I used this custom ogx distro: ``` version: 2 distro_name: starter apis: - file_processors - files - inference - responses - tool_runtime - vector_io providers: inference: - provider_id: openai provider_type: remote::openai config: api_key: ${env.OPENAI_API_KEY:=} base_url: ${env.OPENAI_BASE_URL:=https://api.openai.com/v1} - provider_id: sentence-transformers provider_type: inline::sentence-transformers config: trust_remote_code: true - provider_id: transformers provider_type: inline::transformers vector_io: - provider_id: pgvector provider_type: remote::pgvector config: host: ${env.PGVECTOR_HOST:=localhost} port: ${env.PGVECTOR_PORT:=5432} db: ${env.PGVECTOR_DB:=testvectordb} user: ${env.PGVECTOR_USER:=user} password: ${env.PGVECTOR_PASSWORD:=password} distance_metric: COSINE vector_index: type: HNSW m: 16 ef_construction: 64 ef_search: 40 persistence: namespace: vector_io::pgvector backend: kv_default files: - provider_id: meta-reference-files provider_type: inline::localfs config: storage_dir: ${env.FILES_STORAGE_DIR:=~/.llama/distributions/starter/files} metadata_store: table_name: files_metadata backend: sql_default file_processors: - provider_id: auto provider_type: inline::auto responses: - provider_id: builtin provider_type: inline::builtin config: persistence: agent_state: namespace: agents backend: kv_default responses: table_name: responses backend: sql_default max_write_queue_size: 10000 num_writers: 4 tool_runtime: - provider_id: file-search provider_type: inline::file-search storage: backends: kv_default: type: kv_sqlite db_path: ${env.SQLITE_STORE_DIR:=~/.llama/distributions/starter}/kvstore.db sql_default: type: sql_sqlite db_path: ${env.SQLITE_STORE_DIR:=~/.llama/distributions/starter}/sql_store.db stores: metadata: namespace: registry backend: kv_default inference: table_name: inference_store backend: sql_default max_write_queue_size: 10000 num_writers: 4 conversations: table_name: openai_conversations backend: sql_default prompts: namespace: prompts backend: kv_default connectors: namespace: connectors backend: kv_default registered_resources: models: - metadata: {} model_id: gpt-5.5 provider_id: openai provider_model_id: gpt-5.5 model_type: llm - metadata: embedding_dimension: 768 model_id: nomic-ai/nomic-embed-text-v1.5 provider_id: sentence-transformers provider_model_id: nomic-ai/nomic-embed-text-v1.5 model_type: embedding - metadata: embedding_dimension: 768 model_id: Qwen/Qwen3-Reranker-0.6B provider_id: transformers provider_model_id: Qwen/Qwen3-Reranker-0.6B model_type: rerank vector_dbs: [] tool_groups: - toolgroup_id: builtin::file_search provider_id: file-search server: port: 8321 vector_stores: default_provider_id: pgvector default_embedding_model: provider_id: sentence-transformers model_id: nomic-ai/nomic-embed-text-v1.5 default_reranker_model: provider_id: transformers model_id: Qwen/Qwen3-Reranker-0.6B file_search_params: header_template: 'file_search tool found {num_chunks} chunks: BEGIN of file_search tool results. ' footer_template: 'END of file_search tool results. ' context_prompt_params: chunk_annotation_template: 'Result {index} Content: {chunk.content} Metadata: {metadata} ' context_template: 'The above results were retrieved to help answer the user''s query: "{query}". Use them as supporting information only in answering this query. {annotation_instruction} ' annotation_prompt_params: enable_annotations: true annotation_instruction_template: Cite sources immediately at the end of sentences before punctuation, using `<|file-id|>` format like 'This is a fact <|file-Cn3MSNn72ENTiiq11Qda4A|>.'. Do not add extra punctuation. Use only the file IDs provided, do not invent new ones. chunk_annotation_template: '[{index}] {metadata_text} cite as <|{file_id}|> {chunk_text} ' file_ingestion_params: default_chunk_size_tokens: 512 default_chunk_overlap_tokens: 128 chunk_retrieval_params: chunk_multiplier: 5 max_tokens_in_context: 4000 default_search_mode: hybrid default_reranker_strategy: rrf rrf_impact_factor: 60.0 weighted_search_alpha: 0.5 file_batch_params: max_concurrent_files_per_batch: 3 file_batch_chunk_size: 10 cleanup_interval_seconds: 86400 connectors: [] ``` ### The output after running the script against OGX server: ``` ============================================================ Cleanup: removing ALL vector stores and files via API ============================================================ Existing server found, cleaning up... Deleting vector store: vs_bceebfbd-c41e-42a0-b32f-8abc5df607ff (pgvector-test-invoice) Deleting file: file-56a58b08c93c44fc852fb3c2ab5aff8a (invoicesample.pdf) API cleanup complete ============================================================ Starting fresh OGX server ============================================================ Killing server... Clearing local distribution state... Starting server (log: /tmp/ogx-server-test.log)... Server PID: 40943 Waiting for server... Server ready after 6s Server running. Will stay running after tests complete. ============================================================ Step 1: Upload PDF file ============================================================ File uploaded: file-af746dafd6dc4b198408dc3e28052314 Filename: invoicesample.pdf Size: 149568 bytes ============================================================ Step 2: Create vector store ============================================================ Vector store created: vs_337d1d7b-291e-4056-be9e-2ce94c12723f Status: completed ============================================================ Step 3: Attach file to vector store ============================================================ Attachment status: completed Final status: completed ============================================================ Step 4: Vector Store Search — mode=vector ============================================================ Results count: 1 [1] score=2.9101 | Denny Gunawan 221 Queen St Melbourne VIC 3000 $39.60 123 Somewhere St, Melbourne VIC 3000 (03) 1234 5678 Invoice Number:... -> vector_search: PASS ============================================================ Step 5: Vector Store Search — mode=keyword ============================================================ Results count: 1 [1] score=0.0520 | Denny Gunawan 221 Queen St Melbourne VIC 3000 $39.60 123 Somewhere St, Melbourne VIC 3000 (03) 1234 5678 Invoice Number:... -> keyword_search: PASS ============================================================ Step 6: Vector Store Search — mode=hybrid ============================================================ Results count: 1 [1] score=0.0164 | Denny Gunawan 221 Queen St Melbourne VIC 3000 $39.60 123 Somewhere St, Melbourne VIC 3000 (03) 1234 5678 Invoice Number:... -> hybrid_search: PASS ============================================================ Step 7: Response API with file_search tool ============================================================ NOTE: Responses API uses default_search_mode from server config. To change mode, update run-pgvector-test.yaml and restart server. --- Raw JSON Response --- { "background": false, "created_at": 1778236536, "completed_at": 1778236541, "error": null, "frequency_penalty": 0.0, "id": "resp_5768445a-320c-4f5b-aca9-2673755e32e7", "incomplete_details": null, "model": "openai/gpt-5.5", "object": "response", "output": [ { "id": "fc_357b594b-a147-49c8-ad9b-0951d551b681", "queries": [ "Denny Gunawan invoice number" ], "status": "completed", "type": "file_search_call", "results": [ { "attributes": { "title": "Sunny Farm Invoice Sample", "file_id": "file-af746dafd6dc4b198408dc3e28052314", "chunk_id": "595160ad-647a-6c5c-1211-7218c5fb5d79", "filename": "invoicesample.pdf", "producer": "Prince 16 (www.princexml.com)", "page_count": 1.0, "document_id": "798683ff-cd3b-451b-98f0-eb145b9e2d96", "token_count": 199.0, "chunk_tokenizer": "tiktoken:cl100k_base", "metadata_token_count": 93.0 }, "file_id": "798683ff-cd3b-451b-98f0-eb145b9e2d96", "filename": "798683ff-cd3b-451b-98f0-eb145b9e2d96", "score": 0.03278688524590164, "text": "Denny Gunawan\n221 Queen St\nMelbourne VIC 3000\n$39.60\n123 Somewhere St, Melbourne VIC 3000\n(03) 1234 5678\nInvoice Number: #20130304\nOrganic Items Price/kg Quantity(kg) Subtotal\nApple $5.00 1 $5.00\nOrange $1.99 2 $3.98\nWatermelon $1.69 3 $5.07\nMango $9.56 2 $19.12\nPeach $2.99 1 $2.99\nSubtotal $36.00\nGST (10%) $3.60\nTotal $39.60\n* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sodales\ndapibus fermentum. Nunc adipiscing, magna sed scelerisque cursus, erat\nlectus dapibus urna, sed facilisis leo dui et ipsum." } ] }, { "content": [ { "text": "The invoice number for Denny Gunawan is **#20130304** <|798683ff-cd3b-451b-98f0-eb145b9e2d96|>.", "type": "output_text", "annotations": [], "logprobs": [] } ], "role": "assistant", "type": "message", "id": "msg_c56f096f-5893-4cab-8611-b0b13ab575ce", "status": "completed" } ], "parallel_tool_calls": true, "previous_response_id": null, "prompt_cache_key": null, "prompt": null, "status": "completed", "temperature": 1.0, "text": { "format": { "type": "text" }, "verbosity": null }, "top_p": 1.0, "top_logprobs": 0, "tools": [ { "type": "file_search", "vector_store_ids": [ "vs_337d1d7b-291e-4056-be9e-2ce94c12723f" ], "filters": null, "max_num_results": 10, "ranking_options": null } ], "tool_choice": "auto", "truncation": "disabled", "usage": { "input_tokens": 847, "output_tokens": 81, "total_tokens": 928, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens_details": { "reasoning_tokens": 0 } }, "instructions": null, "max_tool_calls": null, "reasoning": null, "max_output_tokens": null, "safety_identifier": null, "service_tier": "default", "metadata": null, "presence_penalty": 0.0, "store": true } --- End Raw JSON --- Response ID: resp_5768445a-320c-4f5b-aca9-2673755e32e7 Status: completed File search call ID: fc_357b594b-a147-49c8-ad9b-0951d551b681 Status: completed Search results: 1 [1] score=0.0328 file=798683ff-cd3b-451b-98f0-eb145b9e2d96 | Denny Gunawan 221 Queen St Melbourne VIC 3000 $39.60 123 Somewhere St, Melbourne VIC 3000 (03) 1234 ... LLM Response: The invoice number for Denny Gunawan is **#20130304** <|798683ff-cd3b-451b-98f0-eb145b9e2d96|>. -> response_file_search: PASS ============================================================ RESULTS SUMMARY ============================================================ Passed: 4 Failed: 0 All tests passed! ``` ### P.S. if you're curious about test file I used, here it is: [invoicesample.pdf](https://github.com/user-attachments/files/27517019/invoicesample.pdf) <!-- For API changes, include: 1. A testing script (Python, curl, etc.) that exercises the new/modified endpoints 2. The output from running your script Example: ```python ... ... ``` Output: ``` <paste actual output here> ``` --> --------- Signed-off-by: Ian Miller <milleryan2003@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Francisco Javier Arceo <arceofrancisco@gmail.com>
1 parent 25cab0f commit 860c322

9 files changed

Lines changed: 642 additions & 458 deletions

File tree

.github/workflows/integration-vector-io-tests.yml

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,29 @@ jobs:
8585
if: matrix.vector-io-provider == 'remote::pgvector'
8686
run: |
8787
echo "Waiting for Postgres to be ready..."
88-
for _ in {1..30}; do
89-
if docker exec pgvector pg_isready -U ogx > /dev/null 2>&1; then
90-
echo "Postgres is ready!"
88+
for i in {1..30}; do
89+
if docker exec pgvector pg_isready -U ogx -d ogx > /dev/null 2>&1; then
90+
echo "Postgres is ready after ${i}s!"
9191
break
9292
fi
93-
echo "Not ready yet... ($i)"
93+
echo "Not ready yet... (${i})"
9494
sleep 1
9595
done
9696
9797
- name: Enable pgvector extension
9898
if: matrix.vector-io-provider == 'remote::pgvector'
9999
run: |
100-
PGPASSWORD=ogx psql -h localhost -U ogx -d ogx \
101-
-c "CREATE EXTENSION IF NOT EXISTS vector;"
100+
for i in {1..10}; do
101+
if PGPASSWORD=ogx psql -h localhost -U ogx -d ogx \
102+
-c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null; then
103+
echo "pgvector extension enabled"
104+
exit 0
105+
fi
106+
echo "psql not ready yet, retrying... (${i})"
107+
sleep 2
108+
done
109+
echo "Failed to enable pgvector extension after retries"
110+
exit 1
102111
103112
- name: Setup Qdrant
104113
if: matrix.vector-io-provider == 'remote::qdrant'

docs/docs/providers/vector_io/remote_pgvector.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ See [PGVector's documentation](https://github.com/pgvector/pgvector) for more de
236236
| `password` | `str \| None` | No | mysecretpassword | |
237237
| `distance_metric` | `Literal[COSINE, L2, L1, INNER_PRODUCT] \| None` | No | COSINE | PGVector distance metric used for vector search in PGVectorIndex |
238238
| `vector_index` | `PGVectorHNSWVectorIndex \| PGVectorIVFFlatVectorIndex \| None` | No | type=&lt;PGVectorIndexType.HNSW: 'HNSW'&gt; m=16 ef_construction=64 ef_search=40 | PGVector vector index used for Approximate Nearest Neighbor (ANN) search |
239+
| `pool_min_size` | `int` | No | 4 | Minimum number of connections in the asyncpg pool |
240+
| `pool_max_size` | `int` | No | 20 | Maximum number of connections in the asyncpg pool |
241+
| `statement_cache_size` | `int` | No | 512 | Size of the prepared statement cache per connection |
239242
| `persistence` | `KVStoreReference \| None` | No | | Config for KV store backend (SQLite only for now) |
240243
| `persistence.namespace` | `str` | No | | Key prefix for KVStore backends |
241244
| `persistence.backend` | `str` | No | | Name of backend from storage.backends |

pyproject.toml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ starter = [
104104
"opentelemetry-sdk",
105105
"pandas",
106106
"pillow",
107-
"psycopg2-binary",
107+
"pgvector>=0.3.0",
108108
"pymilvus[milvus-lite]>=2.4.10",
109109
"pymongo",
110110
"markitdown[all]",
@@ -200,7 +200,8 @@ unit = [
200200
"ollama",
201201
"aiosqlite",
202202
"aiohttp",
203-
"psycopg2-binary>=2.9.0",
203+
"asyncpg>=0.29.0",
204+
"pgvector>=0.3.0",
204205
"markitdown[all]",
205206
"pypdf>=6.10.2",
206207
"mcp>=1.23.0",
@@ -223,7 +224,8 @@ test = [
223224
"torch>=2.6.0",
224225
"torchvision>=0.21.0",
225226
"chardet",
226-
"psycopg2-binary>=2.9.0",
227+
"asyncpg>=0.29.0",
228+
"pgvector>=0.3.0",
227229
"pypdf>=6.10.2",
228230
"mcp>=1.23.0",
229231
"datasets>=4.0.0",
@@ -577,9 +579,6 @@ module = [
577579
"yaml",
578580
"fire",
579581
"redis.asyncio",
580-
"psycopg2",
581-
"psycopg2.extras",
582-
"psycopg2.extensions",
583582
"torchtune.*",
584583
"fairscale.*",
585584
"torchvision.*",

src/ogx/providers/registry/vector_io.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ def available_providers() -> list[ProviderSpec]:
373373
api=Api.vector_io,
374374
adapter_type="pgvector",
375375
provider_type="remote::pgvector",
376-
pip_packages=["psycopg2-binary"] + DEFAULT_VECTOR_IO_DEPS,
376+
pip_packages=["asyncpg", "pgvector>=0.3.0"] + DEFAULT_VECTOR_IO_DEPS,
377377
module="ogx.providers.remote.vector_io.pgvector",
378378
config_class="ogx.providers.remote.vector_io.pgvector.PGVectorVectorIOConfig",
379379
api_dependencies=[Api.inference],

src/ogx/providers/remote/vector_io/pgvector/config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ class PGVectorVectorIOConfig(BaseModel):
8989
default_factory=PGVectorHNSWVectorIndex,
9090
description="PGVector vector index used for Approximate Nearest Neighbor (ANN) search",
9191
)
92+
pool_min_size: int = Field(default=4, ge=1, description="Minimum number of connections in the asyncpg pool")
93+
pool_max_size: int = Field(default=20, ge=1, description="Maximum number of connections in the asyncpg pool")
94+
statement_cache_size: int = Field(
95+
default=512, ge=0, description="Size of the prepared statement cache per connection"
96+
)
9297
persistence: KVStoreReference | None = Field(
9398
description="Config for KV store backend (SQLite only for now)", default=None
9499
)
@@ -97,6 +102,14 @@ class PGVectorVectorIOConfig(BaseModel):
97102
description="SQL store reference for tenant-isolated vector store metadata",
98103
)
99104

105+
@model_validator(mode="after")
106+
def validate_pool_sizes(self) -> Self:
107+
if self.pool_min_size > self.pool_max_size:
108+
raise ValueError(
109+
f"pool_min_size ({self.pool_min_size}) must be less than or equal to pool_max_size ({self.pool_max_size})"
110+
)
111+
return self
112+
100113
@classmethod
101114
def sample_run_config(
102115
cls,

0 commit comments

Comments
 (0)