From 0c577730d5aeecb5dd07ac4579833edca924f11c Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:25:15 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + SKILL.md | 1292 +++++++++++++++++++++++ plugin.lock.json | 109 ++ references/assistants-api-v2.md | 242 +++++ references/code-interpreter-guide.md | 338 ++++++ references/file-search-rag-guide.md | 364 +++++++ references/migration-from-v1.md | 188 ++++ references/thread-lifecycle.md | 271 +++++ references/top-errors.md | 446 ++++++++ references/vector-stores.md | 422 ++++++++ scripts/check-versions.sh | 49 + templates/basic-assistant.ts | 87 ++ templates/code-interpreter-assistant.ts | 136 +++ templates/file-search-assistant.ts | 213 ++++ templates/function-calling-assistant.ts | 247 +++++ templates/package.json | 23 + templates/streaming-assistant.ts | 165 +++ templates/thread-management.ts | 237 +++++ templates/vector-store-setup.ts | 241 +++++ 20 files changed, 5085 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 SKILL.md create mode 100644 plugin.lock.json create mode 100644 references/assistants-api-v2.md create mode 100644 references/code-interpreter-guide.md create mode 100644 references/file-search-rag-guide.md create mode 100644 references/migration-from-v1.md create mode 100644 references/thread-lifecycle.md create mode 100644 references/top-errors.md create mode 100644 references/vector-stores.md create mode 100755 scripts/check-versions.sh create mode 100644 templates/basic-assistant.ts create mode 100644 templates/code-interpreter-assistant.ts create mode 100644 templates/file-search-assistant.ts create mode 100644 templates/function-calling-assistant.ts create mode 100644 templates/package.json create mode 100644 templates/streaming-assistant.ts create mode 100644 templates/thread-management.ts create mode 100644 templates/vector-store-setup.ts diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..54fc2ea --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "openai-assistants", + "description": "Build stateful chatbots with OpenAI Assistants API v2 - Code Interpreter, File Search (10k files), Function Calling. ⚠️ Sunset H1 2026; use openai-responses for new projects. Use when: building stateful chatbots, implementing RAG, or troubleshooting thread has active run, vector store delays, polling timeouts, or file upload errors.", + "version": "1.0.0", + "author": { + "name": "Jeremy Dawes", + "email": "jeremy@jezweb.net" + }, + "skills": [ + "./" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d044b1 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# openai-assistants + +Build stateful chatbots with OpenAI Assistants API v2 - Code Interpreter, File Search (10k files), Function Calling. ⚠️ Sunset H1 2026; use openai-responses for new projects. Use when: building stateful chatbots, implementing RAG, or troubleshooting thread has active run, vector store delays, polling timeouts, or file upload errors. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..724cd64 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,1292 @@ +--- +name: openai-assistants +description: | + Build stateful chatbots with OpenAI Assistants API v2 - Code Interpreter, File Search (10k files), Function Calling. ⚠️ Sunset H1 2026; use openai-responses for new projects. + + Use when: building stateful chatbots, implementing RAG, or troubleshooting "thread has active run", vector store delays, polling timeouts, or file upload errors. +license: MIT +--- + +# OpenAI Assistants API v2 + +**Status**: Production Ready (Deprecated H1 2026) +**Package**: openai@6.7.0 +**Last Updated**: 2025-10-25 +**v1 Deprecated**: December 18, 2024 +**v2 Sunset**: H1 2026 (migrate to Responses API) + +--- + +## ⚠️ Important: Deprecation Notice + +**OpenAI announced that the Assistants API will be deprecated in favor of the [Responses API](../openai-responses/SKILL.md).** + +**Timeline:** +- ✅ **Dec 18, 2024**: Assistants API v1 deprecated +- ⏳ **H1 2026**: Planned sunset of Assistants API v2 +- ✅ **Now**: Responses API available (recommended for new projects) + +**Should you still use this skill?** +- ✅ **Yes, if**: You have existing Assistants API code (12-18 month migration window) +- ✅ **Yes, if**: You need to maintain legacy applications +- ✅ **Yes, if**: Planning migration from Assistants → Responses +- ❌ **No, if**: Starting a new project (use openai-responses skill instead) + +**Migration Path:** +See `references/migration-to-responses.md` for complete migration guide. + +--- + +## Table of Contents + +1. [Quick Start](#quick-start) +2. [Core Concepts](#core-concepts) +3. [Assistants](#assistants) +4. [Threads](#threads) +5. [Messages](#messages) +6. [Runs](#runs) +7. [Streaming Runs](#streaming-runs) +8. [Tools](#tools) + - [Code Interpreter](#code-interpreter) + - [File Search](#file-search) + - [Function Calling](#function-calling) +9. [Vector Stores](#vector-stores) +10. [File Uploads](#file-uploads) +11. [Thread Lifecycle Management](#thread-lifecycle-management) +12. [Error Handling](#error-handling) +13. [Production Best Practices](#production-best-practices) +14. [Relationship to Other Skills](#relationship-to-other-skills) + +--- + +## Quick Start + +### Installation + +```bash +npm install openai@6.7.0 +``` + +### Environment Setup + +```bash +export OPENAI_API_KEY="sk-..." +``` + +### Basic Assistant (Node.js SDK) + +```typescript +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +// 1. Create an assistant +const assistant = await openai.beta.assistants.create({ + name: "Math Tutor", + instructions: "You are a personal math tutor. Write and run code to answer math questions.", + tools: [{ type: "code_interpreter" }], + model: "gpt-4o", +}); + +// 2. Create a thread +const thread = await openai.beta.threads.create(); + +// 3. Add a message to the thread +await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "I need to solve the equation `3x + 11 = 14`. Can you help me?", +}); + +// 4. Create a run +const run = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, +}); + +// 5. Poll for completion +let runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id); + +while (runStatus.status !== 'completed') { + await new Promise(resolve => setTimeout(resolve, 1000)); + runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id); +} + +// 6. Retrieve messages +const messages = await openai.beta.threads.messages.list(thread.id); +console.log(messages.data[0].content[0].text.value); +``` + +### Basic Assistant (Fetch - Cloudflare Workers) + +```typescript +// 1. Create assistant +const assistant = await fetch('https://api.openai.com/v1/assistants', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${env.OPENAI_API_KEY}`, + 'Content-Type': 'application/json', + 'OpenAI-Beta': 'assistants=v2', + }, + body: JSON.stringify({ + name: "Math Tutor", + instructions: "You are a helpful math tutor.", + model: "gpt-4o", + }), +}); + +const assistantData = await assistant.json(); + +// 2. Create thread +const thread = await fetch('https://api.openai.com/v1/threads', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${env.OPENAI_API_KEY}`, + 'Content-Type': 'application/json', + 'OpenAI-Beta': 'assistants=v2', + }, +}); + +const threadData = await thread.json(); + +// 3. Add message and create run +const run = await fetch(`https://api.openai.com/v1/threads/${threadData.id}/runs`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${env.OPENAI_API_KEY}`, + 'Content-Type': 'application/json', + 'OpenAI-Beta': 'assistants=v2', + }, + body: JSON.stringify({ + assistant_id: assistantData.id, + additional_messages: [{ + role: "user", + content: "What is 3x + 11 = 14?", + }], + }), +}); + +// Poll for completion... +``` + +--- + +## Core Concepts + +The Assistants API uses four main objects: + +### 1. **Assistants** +Configured AI entities with: +- Instructions (system prompt, max 256k characters) +- Model (gpt-4o, gpt-5, etc.) +- Tools (Code Interpreter, File Search, Functions) +- File attachments +- Metadata + +### 2. **Threads** +Conversation containers that: +- Store message history +- Persist across runs +- Can have metadata +- Support up to 100,000 messages + +### 3. **Messages** +Individual messages in a thread: +- User messages (input) +- Assistant messages (output) +- Can include file attachments +- Support text and image content + +### 4. **Runs** +Execution of an assistant on a thread: +- Asynchronous processing +- Multiple states (queued, in_progress, completed, failed, etc.) +- Can stream results +- Handle tool calls automatically + +--- + +## Assistants + +### Create an Assistant + +```typescript +const assistant = await openai.beta.assistants.create({ + name: "Data Analyst", + instructions: "You are a data analyst. Use code interpreter to analyze data and create visualizations.", + model: "gpt-4o", + tools: [ + { type: "code_interpreter" }, + { type: "file_search" }, + ], + tool_resources: { + file_search: { + vector_store_ids: ["vs_abc123"], + }, + }, + metadata: { + department: "analytics", + version: "1.0", + }, +}); +``` + +**Parameters:** +- `model` (required): Model ID (gpt-4o, gpt-5, gpt-4-turbo) +- `instructions`: System prompt (max 256k characters in v2, was 32k in v1) +- `name`: Assistant name (max 256 characters) +- `description`: Description (max 512 characters) +- `tools`: Array of tools (max 128 tools) +- `tool_resources`: Resources for tools (vector stores, files) +- `temperature`: 0-2 (default 1) +- `top_p`: 0-1 (default 1) +- `response_format`: "auto", "json_object", or JSON schema +- `metadata`: Key-value pairs (max 16 pairs) + +### Retrieve an Assistant + +```typescript +const assistant = await openai.beta.assistants.retrieve("asst_abc123"); +``` + +### Update an Assistant + +```typescript +const updatedAssistant = await openai.beta.assistants.update("asst_abc123", { + instructions: "Updated instructions", + tools: [{ type: "code_interpreter" }, { type: "file_search" }], +}); +``` + +### Delete an Assistant + +```typescript +await openai.beta.assistants.del("asst_abc123"); +``` + +### List Assistants + +```typescript +const assistants = await openai.beta.assistants.list({ + limit: 20, + order: "desc", +}); +``` + +--- + +## Threads + +Threads store conversation history and persist across runs. + +### Create a Thread + +```typescript +// Empty thread +const thread = await openai.beta.threads.create(); + +// Thread with initial messages +const thread = await openai.beta.threads.create({ + messages: [ + { + role: "user", + content: "Hello! I need help with Python.", + metadata: { source: "web" }, + }, + ], + metadata: { + user_id: "user_123", + session_id: "session_456", + }, +}); +``` + +### Retrieve a Thread + +```typescript +const thread = await openai.beta.threads.retrieve("thread_abc123"); +``` + +### Update Thread Metadata + +```typescript +const thread = await openai.beta.threads.update("thread_abc123", { + metadata: { + user_id: "user_123", + last_active: new Date().toISOString(), + }, +}); +``` + +### Delete a Thread + +```typescript +await openai.beta.threads.del("thread_abc123"); +``` + +**⚠️ Warning**: Deleting a thread also deletes all messages and runs. Cannot be undone. + +--- + +## Messages + +### Add a Message to a Thread + +```typescript +const message = await openai.beta.threads.messages.create("thread_abc123", { + role: "user", + content: "Can you analyze this data?", + attachments: [ + { + file_id: "file_abc123", + tools: [{ type: "code_interpreter" }], + }, + ], + metadata: { + timestamp: new Date().toISOString(), + }, +}); +``` + +**Parameters:** +- `role`: "user" only (assistant messages created by runs) +- `content`: Text or array of content blocks +- `attachments`: Files with associated tools +- `metadata`: Key-value pairs + +### Retrieve a Message + +```typescript +const message = await openai.beta.threads.messages.retrieve( + "thread_abc123", + "msg_abc123" +); +``` + +### List Messages + +```typescript +const messages = await openai.beta.threads.messages.list("thread_abc123", { + limit: 20, + order: "desc", // "asc" or "desc" +}); + +// Iterate through messages +for (const message of messages.data) { + console.log(`${message.role}: ${message.content[0].text.value}`); +} +``` + +### Update Message Metadata + +```typescript +const message = await openai.beta.threads.messages.update( + "thread_abc123", + "msg_abc123", + { + metadata: { + edited: "true", + edit_timestamp: new Date().toISOString(), + }, + } +); +``` + +### Delete a Message + +```typescript +await openai.beta.threads.messages.del("thread_abc123", "msg_abc123"); +``` + +--- + +## Runs + +Runs execute an assistant on a thread. + +### Create a Run + +```typescript +const run = await openai.beta.threads.runs.create("thread_abc123", { + assistant_id: "asst_abc123", + instructions: "Please address the user as Jane Doe.", + additional_messages: [ + { + role: "user", + content: "Can you help me with this?", + }, + ], +}); +``` + +**Parameters:** +- `assistant_id` (required): Assistant to use +- `instructions`: Override assistant instructions +- `additional_messages`: Add messages before running +- `tools`: Override assistant tools +- `metadata`: Key-value pairs +- `temperature`: Override temperature +- `top_p`: Override top_p +- `max_prompt_tokens`: Limit input tokens +- `max_completion_tokens`: Limit output tokens + +### Retrieve a Run + +```typescript +const run = await openai.beta.threads.runs.retrieve( + "thread_abc123", + "run_abc123" +); + +console.log(run.status); // queued, in_progress, requires_action, completed, failed, etc. +``` + +### Run States + +| State | Description | +|-------|-------------| +| `queued` | Run is waiting to start | +| `in_progress` | Run is executing | +| `requires_action` | Function calling needs your input | +| `cancelling` | Cancellation in progress | +| `cancelled` | Run was cancelled | +| `failed` | Run failed (check `last_error`) | +| `completed` | Run finished successfully | +| `expired` | Run expired (max 10 minutes) | + +### Polling Pattern + +```typescript +async function pollRunCompletion(threadId: string, runId: string) { + let run = await openai.beta.threads.runs.retrieve(threadId, runId); + + while (['queued', 'in_progress', 'cancelling'].includes(run.status)) { + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second + run = await openai.beta.threads.runs.retrieve(threadId, runId); + } + + if (run.status === 'failed') { + throw new Error(`Run failed: ${run.last_error?.message}`); + } + + if (run.status === 'requires_action') { + // Handle function calling (see Function Calling section) + return run; + } + + return run; // completed +} + +const run = await openai.beta.threads.runs.create(threadId, { assistant_id: assistantId }); +const completedRun = await pollRunCompletion(threadId, run.id); +``` + +### Cancel a Run + +```typescript +const run = await openai.beta.threads.runs.cancel("thread_abc123", "run_abc123"); +``` + +**⚠️ Important**: Cancellation is asynchronous. Check `status` becomes `cancelled`. + +### List Runs + +```typescript +const runs = await openai.beta.threads.runs.list("thread_abc123", { + limit: 10, + order: "desc", +}); +``` + +--- + +## Streaming Runs + +Stream run events in real-time using Server-Sent Events (SSE). + +### Basic Streaming + +```typescript +const stream = await openai.beta.threads.runs.stream("thread_abc123", { + assistant_id: "asst_abc123", +}); + +for await (const event of stream) { + if (event.event === 'thread.message.delta') { + const delta = event.data.delta.content?.[0]?.text?.value; + if (delta) { + process.stdout.write(delta); + } + } +} +``` + +### Stream Event Types + +| Event | Description | +|-------|-------------| +| `thread.run.created` | Run was created | +| `thread.run.in_progress` | Run started | +| `thread.run.step.created` | Step created (tool call, message creation) | +| `thread.run.step.delta` | Step progress update | +| `thread.message.created` | Message created | +| `thread.message.delta` | Message content streaming | +| `thread.message.completed` | Message finished | +| `thread.run.completed` | Run finished | +| `thread.run.failed` | Run failed | +| `thread.run.requires_action` | Function calling needed | + +### Complete Streaming Example + +```typescript +async function streamAssistantResponse(threadId: string, assistantId: string) { + const stream = await openai.beta.threads.runs.stream(threadId, { + assistant_id: assistantId, + }); + + for await (const event of stream) { + switch (event.event) { + case 'thread.run.created': + console.log('\\nRun started...'); + break; + + case 'thread.message.delta': + const delta = event.data.delta.content?.[0]; + if (delta?.type === 'text' && delta.text?.value) { + process.stdout.write(delta.text.value); + } + break; + + case 'thread.run.step.delta': + const toolCall = event.data.delta.step_details; + if (toolCall?.type === 'tool_calls') { + const codeInterpreter = toolCall.tool_calls?.[0]?.code_interpreter; + if (codeInterpreter?.input) { + console.log('\\nExecuting code:', codeInterpreter.input); + } + } + break; + + case 'thread.run.completed': + console.log('\\n\\nRun completed!'); + break; + + case 'thread.run.failed': + console.error('\\nRun failed:', event.data.last_error); + break; + + case 'thread.run.requires_action': + // Handle function calling + console.log('\\nFunction calling required'); + break; + } + } +} +``` + +--- + +## Tools + +Assistants API supports three types of tools: + +### Code Interpreter + +Executes Python code in a sandboxed environment. + +**Capabilities:** +- Run Python code +- Generate charts/graphs +- Process files (CSV, JSON, text, images, etc.) +- Return file outputs (images, data files) +- Install packages (limited set available) + +**Example:** + +```typescript +const assistant = await openai.beta.assistants.create({ + name: "Data Analyst", + instructions: "You are a data analyst. Use Python to analyze data and create visualizations.", + model: "gpt-4o", + tools: [{ type: "code_interpreter" }], +}); + +// Upload a file +const file = await openai.files.create({ + file: fs.createReadStream("sales_data.csv"), + purpose: "assistants", +}); + +// Create thread with file +const thread = await openai.beta.threads.create({ + messages: [{ + role: "user", + content: "Analyze this sales data and create a visualization.", + attachments: [{ + file_id: file.id, + tools: [{ type: "code_interpreter" }], + }], + }], +}); + +// Run +const run = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, +}); + +// Poll for completion and retrieve outputs +``` + +**Output Files:** + +Code Interpreter can generate files (images, CSVs, etc.). Access them via: + +```typescript +const messages = await openai.beta.threads.messages.list(thread.id); +const message = messages.data[0]; + +for (const content of message.content) { + if (content.type === 'image_file') { + const fileId = content.image_file.file_id; + const fileContent = await openai.files.content(fileId); + // Save or process file + } +} +``` + +### File Search + +Semantic search over uploaded documents using vector stores. + +**Key Features:** +- Up to 10,000 files per assistant (500x more than v1) +- Automatic chunking and embedding +- Vector + keyword search +- Parallel queries with multi-threading +- Advanced reranking + +**Pricing:** +- $0.10/GB/day for vector storage +- First 1GB free + +**Example:** + +```typescript +// 1. Create vector store +const vectorStore = await openai.beta.vectorStores.create({ + name: "Product Documentation", + metadata: { category: "docs" }, +}); + +// 2. Upload files to vector store +const file = await openai.files.create({ + file: fs.createReadStream("product_guide.pdf"), + purpose: "assistants", +}); + +await openai.beta.vectorStores.files.create(vectorStore.id, { + file_id: file.id, +}); + +// 3. Create assistant with file search +const assistant = await openai.beta.assistants.create({ + name: "Product Support", + instructions: "Use file search to answer questions about our products.", + model: "gpt-4o", + tools: [{ type: "file_search" }], + tool_resources: { + file_search: { + vector_store_ids: [vectorStore.id], + }, + }, +}); + +// 4. Create thread and run +const thread = await openai.beta.threads.create({ + messages: [{ + role: "user", + content: "How do I install the product?", + }], +}); + +const run = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, +}); +``` + +**Best Practices:** +- Wait for vector store status to be `completed` before using +- Use metadata for filtering (coming soon) +- Chunk large documents appropriately +- Monitor storage costs + +### Function Calling + +Define custom functions for the assistant to call. + +**Example:** + +```typescript +const assistant = await openai.beta.assistants.create({ + name: "Weather Assistant", + instructions: "You help users get weather information.", + model: "gpt-4o", + tools: [{ + type: "function", + function: { + name: "get_weather", + description: "Get the current weather for a location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "City name, e.g., 'San Francisco'", + }, + unit: { + type: "string", + enum: ["celsius", "fahrenheit"], + description: "Temperature unit", + }, + }, + required: ["location"], + }, + }, + }], +}); + +// Create thread and run +const thread = await openai.beta.threads.create({ + messages: [{ + role: "user", + content: "What's the weather in San Francisco?", + }], +}); + +let run = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, +}); + +// Poll until requires_action +while (run.status === 'in_progress' || run.status === 'queued') { + await new Promise(resolve => setTimeout(resolve, 1000)); + run = await openai.beta.threads.runs.retrieve(thread.id, run.id); +} + +if (run.status === 'requires_action') { + const toolCalls = run.required_action.submit_tool_outputs.tool_calls; + + const toolOutputs = []; + for (const toolCall of toolCalls) { + if (toolCall.function.name === 'get_weather') { + const args = JSON.parse(toolCall.function.arguments); + // Call your actual weather API + const weather = await getWeatherAPI(args.location, args.unit); + + toolOutputs.push({ + tool_call_id: toolCall.id, + output: JSON.stringify(weather), + }); + } + } + + // Submit tool outputs + run = await openai.beta.threads.runs.submitToolOutputs(thread.id, run.id, { + tool_outputs: toolOutputs, + }); + + // Continue polling... +} +``` + +--- + +## Vector Stores + +Vector stores enable efficient semantic search over large document collections. + +### Create a Vector Store + +```typescript +const vectorStore = await openai.beta.vectorStores.create({ + name: "Legal Documents", + metadata: { + department: "legal", + category: "contracts", + }, + expires_after: { + anchor: "last_active_at", + days: 7, // Auto-delete 7 days after last use + }, +}); +``` + +### Add Files to Vector Store + +**Single File:** + +```typescript +const file = await openai.files.create({ + file: fs.createReadStream("contract.pdf"), + purpose: "assistants", +}); + +await openai.beta.vectorStores.files.create(vectorStore.id, { + file_id: file.id, +}); +``` + +**Batch Upload:** + +```typescript +const fileBatch = await openai.beta.vectorStores.fileBatches.create(vectorStore.id, { + file_ids: ["file_abc123", "file_def456", "file_ghi789"], +}); + +// Poll for batch completion +let batch = await openai.beta.vectorStores.fileBatches.retrieve(vectorStore.id, fileBatch.id); +while (batch.status === 'in_progress') { + await new Promise(resolve => setTimeout(resolve, 1000)); + batch = await openai.beta.vectorStores.fileBatches.retrieve(vectorStore.id, fileBatch.id); +} +``` + +### Check Vector Store Status + +```typescript +const vectorStore = await openai.beta.vectorStores.retrieve("vs_abc123"); + +console.log(vectorStore.status); // "in_progress", "completed", "failed" +console.log(vectorStore.file_counts); // { in_progress: 0, completed: 50, failed: 0 } +``` + +**⚠️ Important**: Wait for `status: "completed"` before using with file search. + +### List Vector Stores + +```typescript +const stores = await openai.beta.vectorStores.list({ + limit: 20, + order: "desc", +}); +``` + +### Update Vector Store + +```typescript +const vectorStore = await openai.beta.vectorStores.update("vs_abc123", { + name: "Updated Name", + metadata: { updated: "true" }, +}); +``` + +### Delete Vector Store + +```typescript +await openai.beta.vectorStores.del("vs_abc123"); +``` + +--- + +## File Uploads + +Upload files for use with Code Interpreter or File Search. + +### Upload a File + +```typescript +import fs from 'fs'; + +const file = await openai.files.create({ + file: fs.createReadStream("document.pdf"), + purpose: "assistants", +}); + +console.log(file.id); // file_abc123 +``` + +**Supported Formats:** +- **Code Interpreter**: .c, .cpp, .csv, .docx, .html, .java, .json, .md, .pdf, .php, .pptx, .py, .rb, .tex, .txt, .css, .jpeg, .jpg, .js, .gif, .png, .tar, .ts, .xlsx, .xml, .zip +- **File Search**: .c, .cpp, .docx, .html, .java, .json, .md, .pdf, .php, .pptx, .py, .rb, .tex, .txt, .css, .js, .ts, .go + +**Size Limits:** +- Code Interpreter: 512 MB per file +- File Search: 512 MB per file +- Vector Store: Up to 10,000 files + +### Retrieve File Info + +```typescript +const file = await openai.files.retrieve("file_abc123"); +``` + +### Download File Content + +```typescript +const content = await openai.files.content("file_abc123"); +// Returns binary content +``` + +### Delete a File + +```typescript +await openai.files.del("file_abc123"); +``` + +### List Files + +```typescript +const files = await openai.files.list({ + purpose: "assistants", +}); +``` + +--- + +## Thread Lifecycle Management + +Proper thread lifecycle management prevents common errors. + +### Pattern 1: One Thread Per User + +```typescript +async function getOrCreateUserThread(userId: string): Promise { + // Check if thread exists in your database + let threadId = await db.getThreadIdForUser(userId); + + if (!threadId) { + // Create new thread + const thread = await openai.beta.threads.create({ + metadata: { user_id: userId }, + }); + threadId = thread.id; + await db.saveThreadIdForUser(userId, threadId); + } + + return threadId; +} +``` + +### Pattern 2: Active Run Check + +```typescript +async function ensureNoActiveRun(threadId: string) { + const runs = await openai.beta.threads.runs.list(threadId, { + limit: 1, + order: "desc", + }); + + const latestRun = runs.data[0]; + if (latestRun && ['queued', 'in_progress', 'cancelling'].includes(latestRun.status)) { + throw new Error('Thread already has an active run. Wait or cancel first.'); + } +} + +// Before creating new run +await ensureNoActiveRun(threadId); +const run = await openai.beta.threads.runs.create(threadId, { assistant_id }); +``` + +### Pattern 3: Thread Cleanup + +```typescript +async function cleanupOldThreads(maxAgeHours = 24) { + const threads = await openai.beta.threads.list({ limit: 100 }); + + for (const thread of threads.data) { + const createdAt = new Date(thread.created_at * 1000); + const ageHours = (Date.now() - createdAt.getTime()) / (1000 * 60 * 60); + + if (ageHours > maxAgeHours) { + await openai.beta.threads.del(thread.id); + } + } +} +``` + +--- + +## Error Handling + +### Common Errors and Solutions + +**1. Thread Already Has Active Run** + +``` +Error: 400 Can't add messages to thread_xxx while a run run_xxx is active. +``` + +**Solution:** +```typescript +// Wait for run to complete or cancel it +const run = await openai.beta.threads.runs.retrieve(threadId, runId); +if (['queued', 'in_progress'].includes(run.status)) { + await openai.beta.threads.runs.cancel(threadId, runId); + // Wait for cancellation + while (run.status !== 'cancelled') { + await new Promise(resolve => setTimeout(resolve, 500)); + run = await openai.beta.threads.runs.retrieve(threadId, runId); + } +} +``` + +**2. Run Polling Timeout** + +Long-running tasks may exceed reasonable polling windows. + +**Solution:** +```typescript +async function pollWithTimeout(threadId: string, runId: string, maxSeconds = 300) { + const startTime = Date.now(); + + while (true) { + const run = await openai.beta.threads.runs.retrieve(threadId, runId); + + if (!['queued', 'in_progress'].includes(run.status)) { + return run; + } + + const elapsed = (Date.now() - startTime) / 1000; + if (elapsed > maxSeconds) { + await openai.beta.threads.runs.cancel(threadId, runId); + throw new Error('Run exceeded timeout'); + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } +} +``` + +**3. Vector Store Not Ready** + +Using vector store before indexing completes. + +**Solution:** +```typescript +async function waitForVectorStore(vectorStoreId: string) { + let store = await openai.beta.vectorStores.retrieve(vectorStoreId); + + while (store.status === 'in_progress') { + await new Promise(resolve => setTimeout(resolve, 2000)); + store = await openai.beta.vectorStores.retrieve(vectorStoreId); + } + + if (store.status === 'failed') { + throw new Error('Vector store indexing failed'); + } + + return store; +} +``` + +**4. File Upload Format Issues** + +Unsupported file formats cause errors. + +**Solution:** +```typescript +const SUPPORTED_FORMATS = { + code_interpreter: ['.csv', '.json', '.pdf', '.txt', '.py', '.js', '.xlsx'], + file_search: ['.pdf', '.docx', '.txt', '.md', '.html'], +}; + +function validateFile(filename: string, tool: string) { + const ext = filename.substring(filename.lastIndexOf('.')).toLowerCase(); + if (!SUPPORTED_FORMATS[tool].includes(ext)) { + throw new Error(`Unsupported file format for ${tool}: ${ext}`); + } +} +``` + +See `references/top-errors.md` for complete error catalog. + +--- + +## Production Best Practices + +### 1. Use Assistant IDs (Don't Recreate) + +**❌ Bad:** +```typescript +// Creates new assistant on every request! +const assistant = await openai.beta.assistants.create({ ... }); +``` + +**✅ Good:** +```typescript +// Create once, store ID, reuse +const ASSISTANT_ID = process.env.ASSISTANT_ID || await createAssistant(); + +async function createAssistant() { + const assistant = await openai.beta.assistants.create({ ... }); + console.log('Save this ID:', assistant.id); + return assistant.id; +} +``` + +### 2. Implement Proper Error Handling + +```typescript +async function createRunWithRetry(threadId: string, assistantId: string, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await openai.beta.threads.runs.create(threadId, { + assistant_id: assistantId, + }); + } catch (error) { + if (error.status === 429) { + // Rate limit - wait and retry + await new Promise(resolve => setTimeout(resolve, 2000 * (i + 1))); + continue; + } + + if (error.message?.includes('active run')) { + // Wait for active run to complete + await new Promise(resolve => setTimeout(resolve, 5000)); + continue; + } + + throw error; // Other errors + } + } + + throw new Error('Max retries exceeded'); +} +``` + +### 3. Monitor Costs + +```typescript +// Track usage +const run = await openai.beta.threads.runs.retrieve(threadId, runId); +console.log('Tokens used:', run.usage); +// { prompt_tokens: 150, completion_tokens: 200, total_tokens: 350 } + +// Set limits +const run = await openai.beta.threads.runs.create(threadId, { + assistant_id: assistantId, + max_prompt_tokens: 1000, + max_completion_tokens: 500, +}); +``` + +### 4. Clean Up Resources + +```typescript +// Delete old threads +async function cleanupUserThread(userId: string) { + const threadId = await db.getThreadIdForUser(userId); + if (threadId) { + await openai.beta.threads.del(threadId); + await db.deleteThreadIdForUser(userId); + } +} + +// Delete unused vector stores +async function cleanupVectorStores(keepDays = 30) { + const stores = await openai.beta.vectorStores.list({ limit: 100 }); + + for (const store of stores.data) { + const ageSeconds = Date.now() / 1000 - store.created_at; + const ageDays = ageSeconds / (60 * 60 * 24); + + if (ageDays > keepDays) { + await openai.beta.vectorStores.del(store.id); + } + } +} +``` + +### 5. Use Streaming for Better UX + +```typescript +// Show progress in real-time +async function streamToUser(threadId: string, assistantId: string) { + const stream = await openai.beta.threads.runs.stream(threadId, { + assistant_id: assistantId, + }); + + for await (const event of stream) { + if (event.event === 'thread.message.delta') { + const delta = event.data.delta.content?.[0]?.text?.value; + if (delta) { + // Send to user immediately + sendToClient(delta); + } + } + } +} +``` + +--- + +## Relationship to Other Skills + +### vs. openai-api Skill + +**openai-api** (Chat Completions): +- Stateless requests +- Manual history management +- Direct responses +- Use for: Simple text generation, function calling + +**openai-assistants**: +- Stateful conversations (threads) +- Automatic history management +- Built-in tools (Code Interpreter, File Search) +- Use for: Chatbots, data analysis, RAG + +### vs. openai-responses Skill + +**openai-responses** (Responses API): +- ✅ **Recommended for new projects** +- Better reasoning preservation +- Modern MCP integration +- Active development + +**openai-assistants**: +- ⚠️ **Deprecated in H1 2026** +- Use for legacy apps +- Migration path available + +**Migration:** See `references/migration-to-responses.md` + +--- + +## Migration from v1 to v2 + +**v1 deprecated**: December 18, 2024 + +**Key Changes:** +1. **Retrieval → File Search**: `retrieval` tool replaced with `file_search` +2. **Vector Stores**: Files now organized in vector stores (10,000 file limit) +3. **Instructions Limit**: Increased from 32k to 256k characters +4. **File Attachments**: Now message-level instead of assistant-level + +See `references/migration-from-v1.md` for complete guide. + +--- + +## Next Steps + +**Templates:** +- `templates/basic-assistant.ts` - Simple math tutor +- `templates/code-interpreter-assistant.ts` - Data analysis +- `templates/file-search-assistant.ts` - RAG with vector stores +- `templates/function-calling-assistant.ts` - Custom tools +- `templates/streaming-assistant.ts` - Real-time streaming + +**References:** +- `references/top-errors.md` - 12 common errors and solutions +- `references/thread-lifecycle.md` - Thread management patterns +- `references/vector-stores.md` - Vector store deep dive + +**Related Skills:** +- `openai-responses` - Modern replacement (recommended) +- `openai-api` - Chat Completions (stateless) + +--- + +**Last Updated**: 2025-10-25 +**Package Version**: openai@6.7.0 +**Status**: Production Ready (Deprecated H1 2026) diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..3ee855c --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,109 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:jezweb/claude-skills:skills/openai-assistants", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "044bfce173a2cbf723d28b7853c92379476da6a2", + "treeHash": "ff884027d18fceda67c0432d9c653abaa070f9727c7792c2400a0447de04412b", + "generatedAt": "2025-11-28T10:19:01.147312Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "openai-assistants", + "description": "Build stateful chatbots with OpenAI Assistants API v2 - Code Interpreter, File Search (10k files), Function Calling. ⚠️ Sunset H1 2026; use openai-responses for new projects. Use when: building stateful chatbots, implementing RAG, or troubleshooting thread has active run, vector store delays, polling timeouts, or file upload errors.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "c5898c0b70530d8d842fa4834d2f054401bf8cefe1a2267b28bd28c57be7bcc7" + }, + { + "path": "SKILL.md", + "sha256": "1dc400edc193d07ae79474a518c685accf1b31131895b60057f832fbed6c5c43" + }, + { + "path": "references/top-errors.md", + "sha256": "e2b06393f97eff808b9c215e3df66c8ac8c4f9e3c5592f62544d49fc99ccaf6b" + }, + { + "path": "references/assistants-api-v2.md", + "sha256": "44503832782dc6bab67006234bd906b066e8853ae85568c7ea4b9fd174488526" + }, + { + "path": "references/code-interpreter-guide.md", + "sha256": "66ce1a7fba0093a72885aaa845fe0b6f8fe0dd3e8ab5e4e3c4ade8e19ac7f335" + }, + { + "path": "references/migration-from-v1.md", + "sha256": "d4ba13447a2979bbcb8c191efbb2b5297e21f4fb9ed68057d6994a07b71e26f3" + }, + { + "path": "references/file-search-rag-guide.md", + "sha256": "4ebb60d3010e3f0c895a6527e8373f5acc197b3a3c7264147616ef7fe2e3ffb7" + }, + { + "path": "references/vector-stores.md", + "sha256": "371cfdd07941bfef867081af3cb7992b2e059e2b2bc1f708e5f574cc7d8713b0" + }, + { + "path": "references/thread-lifecycle.md", + "sha256": "01fbc10480407f3930b2b5996fa725e7ec348187069fe952cbbc2648294930c3" + }, + { + "path": "scripts/check-versions.sh", + "sha256": "ee917b26e7b8c5afbd0c52e6465929e10efb4f7be086d3497fc00fa365677a63" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "cf72bb9c4bfad1c013dc123718e612439d656ded9e1250e047fe86ae5433f2b0" + }, + { + "path": "templates/vector-store-setup.ts", + "sha256": "9087fa7a3dea50731c5446b906aca8a63c2c0aa241dd1525ff7e294f3abdfa3e" + }, + { + "path": "templates/code-interpreter-assistant.ts", + "sha256": "f19e9a37635d9048a9da6e0e01c5c99f7c27cdd47b813f71aff4a6a7da8b93ce" + }, + { + "path": "templates/basic-assistant.ts", + "sha256": "439c713f0b0cb45ed039d15cb544221be5244c80fadd47bcf86d51596eac5834" + }, + { + "path": "templates/file-search-assistant.ts", + "sha256": "565102a29f23bb5d481a2347f3e0386148cedd40d22524415814ebbbf287a5c0" + }, + { + "path": "templates/streaming-assistant.ts", + "sha256": "ae871b3b2855c93e8c6a4ba6d736e7cfb34bf4b552d9ac989b13ae6d618f8eb3" + }, + { + "path": "templates/thread-management.ts", + "sha256": "701be59628cfcf8c3c699be04c48b21aa6e90186945360cf30a13f7da8fa99f1" + }, + { + "path": "templates/package.json", + "sha256": "7cfdc0841a770146d55b7ae0142ebcd5625f9296bd436f516b45f278143c60bd" + }, + { + "path": "templates/function-calling-assistant.ts", + "sha256": "2f3da2dbd8ccdbed3ceab4b9af915dbb01272e7c6c9a04d13cf51f23e490c265" + } + ], + "dirSha256": "ff884027d18fceda67c0432d9c653abaa070f9727c7792c2400a0447de04412b" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/references/assistants-api-v2.md b/references/assistants-api-v2.md new file mode 100644 index 0000000..313193f --- /dev/null +++ b/references/assistants-api-v2.md @@ -0,0 +1,242 @@ +# Assistants API v2 - Complete Overview + +**Version**: v2 (v1 deprecated Dec 18, 2024) +**Status**: Production (Deprecated H1 2026) +**Replacement**: [Responses API](../../openai-responses/SKILL.md) + +--- + +## Architecture + +The Assistants API provides stateful conversational AI through four main objects: + +``` +Assistant (configured AI entity) + ↓ +Thread (conversation container) + ↓ +Messages (user + assistant messages) + ↓ +Runs (execution on thread) +``` + +--- + +## Core Objects + +### 1. Assistants + +Configured AI entities with: +- **Instructions**: System prompt (max 256k characters) +- **Model**: gpt-4o, gpt-5, etc. +- **Tools**: code_interpreter, file_search, functions +- **Tool Resources**: Vector stores, files +- **Metadata**: Custom key-value pairs + +**Lifecycle**: Create once, reuse many times. + +### 2. Threads + +Conversation containers: +- **Persistent**: Store entire conversation history +- **Capacity**: Up to 100,000 messages +- **Reusable**: One thread per user for continuity +- **Metadata**: Track ownership, session info + +### 3. Messages + +Individual conversation turns: +- **Roles**: user, assistant +- **Content**: Text, images, files +- **Attachments**: Files with tool associations +- **Metadata**: Custom tracking info + +### 4. Runs + +Asynchronous execution: +- **States**: queued → in_progress → completed/failed/requires_action +- **Streaming**: Real-time SSE events +- **Tool Calls**: Automatic handling or requires_action +- **Timeouts**: 10-minute max execution + +--- + +## Workflow Patterns + +### Basic Pattern + +```typescript +// 1. Create assistant (once) +const assistant = await openai.beta.assistants.create({...}); + +// 2. Create thread (per conversation) +const thread = await openai.beta.threads.create(); + +// 3. Add message +await openai.beta.threads.messages.create(thread.id, {...}); + +// 4. Run +const run = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, +}); + +// 5. Poll for completion +while (run.status !== 'completed') { + await sleep(1000); + run = await openai.beta.threads.runs.retrieve(thread.id, run.id); +} + +// 6. Get response +const messages = await openai.beta.threads.messages.list(thread.id); +``` + +### Streaming Pattern + +```typescript +const stream = await openai.beta.threads.runs.stream(thread.id, { + assistant_id: assistant.id, +}); + +for await (const event of stream) { + if (event.event === 'thread.message.delta') { + process.stdout.write(event.data.delta.content?.[0]?.text?.value || ''); + } +} +``` + +--- + +## Tools + +### 1. Code Interpreter + +- **Purpose**: Execute Python code +- **Capabilities**: Data analysis, charts, file processing +- **File Support**: CSV, JSON, images, etc. +- **Outputs**: Text logs, image files + +### 2. File Search + +- **Purpose**: Semantic search over documents +- **Capacity**: Up to 10,000 files per assistant +- **Technology**: Vector + keyword search +- **Pricing**: $0.10/GB/day (first 1GB free) + +### 3. Function Calling + +- **Purpose**: Custom tools integration +- **Pattern**: requires_action → submit_tool_outputs +- **Timeout**: Must respond within 10 minutes +- **Parallel**: Multiple functions can be called at once + +--- + +## Key Limits + +| Resource | Limit | +|----------|-------| +| Assistant instructions | 256,000 characters | +| Thread messages | 100,000 per thread | +| Tools per assistant | 128 tools | +| Vector store files | 10,000 per assistant | +| File size | 512 MB per file | +| Run execution time | 10 minutes | +| Metadata pairs | 16 per object | + +--- + +## Pricing + +### API Calls +- Same as Chat Completions (pay per token) +- Run usage reported in `run.usage` + +### Vector Stores +- **Storage**: $0.10/GB/day +- **Free tier**: First 1GB +- **Auto-expiration**: Configurable + +--- + +## Migration Timeline + +- **✅ Dec 18, 2024**: v1 deprecated (no longer accessible) +- **⏳ H1 2026**: v2 planned sunset +- **✅ Now**: Responses API available (recommended replacement) + +**Action**: Plan migration to Responses API for new projects. + +--- + +## Best Practices + +1. **Reuse Assistants**: Create once, use many times +2. **One Thread Per User**: Maintain conversation continuity +3. **Check Active Runs**: Before creating new runs +4. **Stream for UX**: Better user experience than polling +5. **Set Timeouts**: Prevent infinite polling +6. **Clean Up**: Delete old threads and vector stores +7. **Monitor Costs**: Track token usage and storage + +--- + +## Common Patterns + +### Multi-User Chatbot +```typescript +const userThreads = new Map(); + +async function getUserThread(userId: string) { + if (!userThreads.has(userId)) { + const thread = await openai.beta.threads.create({ + metadata: { user_id: userId }, + }); + userThreads.set(userId, thread.id); + } + return userThreads.get(userId)!; +} +``` + +### RAG Application +```typescript +// 1. Create vector store with documents +const vectorStore = await openai.beta.vectorStores.create({...}); +await openai.beta.vectorStores.fileBatches.create(vectorStore.id, {...}); + +// 2. Create assistant with file_search +const assistant = await openai.beta.assistants.create({ + tools: [{ type: "file_search" }], + tool_resources: { + file_search: { vector_store_ids: [vectorStore.id] }, + }, +}); +``` + +### Data Analysis +```typescript +const assistant = await openai.beta.assistants.create({ + tools: [{ type: "code_interpreter" }], +}); + +// Upload data +const file = await openai.files.create({...}); + +// Attach to message +await openai.beta.threads.messages.create(thread.id, { + content: "Analyze this data", + attachments: [{ file_id: file.id, tools: [{ type: "code_interpreter" }] }], +}); +``` + +--- + +## Official Documentation + +- **API Reference**: https://platform.openai.com/docs/api-reference/assistants +- **Overview**: https://platform.openai.com/docs/assistants/overview +- **Tools**: https://platform.openai.com/docs/assistants/tools +- **Migration**: https://platform.openai.com/docs/assistants/whats-new + +--- + +**Last Updated**: 2025-10-25 diff --git a/references/code-interpreter-guide.md b/references/code-interpreter-guide.md new file mode 100644 index 0000000..b16e29d --- /dev/null +++ b/references/code-interpreter-guide.md @@ -0,0 +1,338 @@ +# Code Interpreter Guide + +Complete guide to using Code Interpreter with the Assistants API. + +--- + +## What is Code Interpreter? + +A built-in tool that executes Python code in a sandboxed environment, enabling: +- Data analysis and processing +- Mathematical computations +- Chart and graph generation +- File parsing (CSV, JSON, Excel, etc.) +- Data transformations + +--- + +## Setup + +```typescript +const assistant = await openai.beta.assistants.create({ + name: "Data Analyst", + instructions: "You analyze data and create visualizations.", + tools: [{ type: "code_interpreter" }], + model: "gpt-4o", +}); +``` + +--- + +## File Uploads + +### Upload Data Files + +```typescript +const file = await openai.files.create({ + file: fs.createReadStream("data.csv"), + purpose: "assistants", +}); +``` + +### Attach to Messages + +```typescript +await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "Analyze this sales data", + attachments: [{ + file_id: file.id, + tools: [{ type: "code_interpreter" }], + }], +}); +``` + +--- + +## Supported File Formats + +**Data Files**: +- `.csv`, `.json`, `.xlsx` - Tabular data +- `.txt`, `.md` - Text files +- `.pdf`, `.docx`, `.pptx` - Documents (text extraction) + +**Code Files**: +- `.py`, `.js`, `.ts`, `.java`, `.cpp` - Source code + +**Images** (for processing, not vision): +- `.png`, `.jpg`, `.jpeg`, `.gif` - Image manipulation + +**Archives**: +- `.zip`, `.tar` - Compressed files + +**Size Limit**: 512 MB per file + +--- + +## Common Use Cases + +### 1. Data Analysis + +```typescript +const thread = await openai.beta.threads.create({ + messages: [{ + role: "user", + content: "Calculate the average, median, and standard deviation of the revenue column", + attachments: [{ + file_id: csvFileId, + tools: [{ type: "code_interpreter" }], + }], + }], +}); +``` + +### 2. Data Visualization + +```typescript +await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "Create a line chart showing revenue over time", +}); + +// After run completes, download the generated image +const messages = await openai.beta.threads.messages.list(thread.id); +for (const content of messages.data[0].content) { + if (content.type === 'image_file') { + const imageData = await openai.files.content(content.image_file.file_id); + const buffer = Buffer.from(await imageData.arrayBuffer()); + fs.writeFileSync('chart.png', buffer); + } +} +``` + +### 3. File Conversion + +```typescript +await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "Convert this Excel file to CSV format", + attachments: [{ + file_id: excelFileId, + tools: [{ type: "code_interpreter" }], + }], +}); +``` + +--- + +## Retrieving Outputs + +### Text Output + +```typescript +const messages = await openai.beta.threads.messages.list(thread.id); +const response = messages.data[0]; + +for (const content of response.content) { + if (content.type === 'text') { + console.log(content.text.value); + } +} +``` + +### Generated Files (Charts, CSVs) + +```typescript +for (const content of response.content) { + if (content.type === 'image_file') { + const fileId = content.image_file.file_id; + const data = await openai.files.content(fileId); + const buffer = Buffer.from(await data.arrayBuffer()); + fs.writeFileSync(`output_${fileId}.png`, buffer); + } +} +``` + +### Execution Logs + +```typescript +const runSteps = await openai.beta.threads.runs.steps.list(thread.id, run.id); + +for (const step of runSteps.data) { + if (step.step_details.type === 'tool_calls') { + for (const toolCall of step.step_details.tool_calls) { + if (toolCall.type === 'code_interpreter') { + console.log('Code:', toolCall.code_interpreter.input); + console.log('Output:', toolCall.code_interpreter.outputs); + } + } + } +} +``` + +--- + +## Python Environment + +### Available Libraries + +The Code Interpreter sandbox includes common libraries: +- **Data**: pandas, numpy +- **Math**: scipy, sympy +- **Plotting**: matplotlib, seaborn +- **ML**: scikit-learn (limited) +- **Utils**: requests, PIL, csv, json + +**Note**: Not all PyPI packages available. Use standard library where possible. + +### Environment Limits + +- **Execution Time**: Part of 10-minute run limit +- **Memory**: Limited (exact amount not documented) +- **Disk Space**: Files persist during run only +- **Network**: No outbound internet access + +--- + +## Best Practices + +### 1. Clear Instructions + +```typescript +// ❌ Vague +"Analyze the data" + +// ✅ Specific +"Calculate the mean, median, and mode for each numeric column. Create a bar chart comparing these metrics." +``` + +### 2. File Download Immediately + +```typescript +// Generated files are temporary - download right after completion +if (run.status === 'completed') { + const messages = await openai.beta.threads.messages.list(thread.id); + // Download all image files immediately + for (const message of messages.data) { + for (const content of message.content) { + if (content.type === 'image_file') { + await downloadFile(content.image_file.file_id); + } + } + } +} +``` + +### 3. Error Handling + +```typescript +const runSteps = await openai.beta.threads.runs.steps.list(thread.id, run.id); + +for (const step of runSteps.data) { + if (step.step_details.type === 'tool_calls') { + for (const toolCall of step.step_details.tool_calls) { + if (toolCall.type === 'code_interpreter') { + const outputs = toolCall.code_interpreter.outputs; + for (const output of outputs) { + if (output.type === 'logs' && output.logs.includes('Error')) { + console.error('Execution error:', output.logs); + } + } + } + } + } +} +``` + +--- + +## Common Patterns + +### Pattern: Iterative Analysis + +```typescript +// 1. Upload data +const file = await openai.files.create({...}); + +// 2. Initial analysis +await sendMessage("What are the columns and data types?"); + +// 3. Follow-up based on results +await sendMessage("Show the distribution of the 'category' column"); + +// 4. Visualization +await sendMessage("Create a heatmap of correlations between numeric columns"); +``` + +### Pattern: Multi-File Processing + +```typescript +await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "Merge these two CSV files on the 'id' column", + attachments: [ + { file_id: file1Id, tools: [{ type: "code_interpreter" }] }, + { file_id: file2Id, tools: [{ type: "code_interpreter" }] }, + ], +}); +``` + +--- + +## Troubleshooting + +### Issue: Code Execution Fails + +**Symptoms**: Run completes but no output/error in logs + +**Solutions**: +- Check file format compatibility +- Verify file isn't corrupted +- Ensure data is in expected format (headers, encoding) +- Try simpler request first to verify setup + +### Issue: Generated Files Not Found + +**Symptoms**: `image_file.file_id` doesn't exist + +**Solutions**: +- Download immediately after run completes +- Check run steps for actual outputs +- Verify code execution succeeded + +### Issue: Timeout on Large Files + +**Symptoms**: Run exceeds 10-minute limit + +**Solutions**: +- Split large files into smaller chunks +- Request specific analysis (not "analyze everything") +- Use sampling for exploratory analysis + +--- + +## Example Prompts + +**Data Exploration**: +- "Summarize this dataset: shape, columns, data types, missing values" +- "Show the first 10 rows" +- "What are the unique values in the 'status' column?" + +**Statistical Analysis**: +- "Calculate descriptive statistics for all numeric columns" +- "Perform correlation analysis between price and quantity" +- "Detect outliers using the IQR method" + +**Visualization**: +- "Create a histogram of the 'age' distribution" +- "Plot revenue trends over time with a moving average" +- "Generate a scatter plot of height vs weight, colored by gender" + +**Data Transformation**: +- "Remove rows with missing values" +- "Normalize the 'sales' column to 0-1 range" +- "Convert dates to YYYY-MM-DD format" + +--- + +**Last Updated**: 2025-10-25 diff --git a/references/file-search-rag-guide.md b/references/file-search-rag-guide.md new file mode 100644 index 0000000..544327c --- /dev/null +++ b/references/file-search-rag-guide.md @@ -0,0 +1,364 @@ +# File Search & RAG Guide + +Complete guide to implementing Retrieval-Augmented Generation (RAG) with the Assistants API. + +--- + +## What is File Search? + +A built-in tool for semantic search over documents using vector stores: +- **Capacity**: Up to 10,000 files per assistant (vs 20 in v1) +- **Technology**: Vector + keyword search with reranking +- **Automatic**: Chunking, embedding, and indexing handled by OpenAI +- **Pricing**: $0.10/GB/day (first 1GB free) + +--- + +## Architecture + +``` +Documents (PDF, DOCX, MD, etc.) + ↓ +Vector Store (chunking + embeddings) + ↓ +Assistant with file_search tool + ↓ +Semantic Search + Reranking + ↓ +Retrieved Context + LLM Generation +``` + +--- + +## Quick Setup + +### 1. Create Vector Store + +```typescript +const vectorStore = await openai.beta.vectorStores.create({ + name: "Product Documentation", + expires_after: { + anchor: "last_active_at", + days: 30, + }, +}); +``` + +### 2. Upload Documents + +```typescript +const files = await Promise.all([ + openai.files.create({ file: fs.createReadStream("doc1.pdf"), purpose: "assistants" }), + openai.files.create({ file: fs.createReadStream("doc2.md"), purpose: "assistants" }), +]); + +const batch = await openai.beta.vectorStores.fileBatches.create(vectorStore.id, { + file_ids: files.map(f => f.id), +}); +``` + +### 3. Wait for Indexing + +```typescript +let batch = await openai.beta.vectorStores.fileBatches.retrieve(vectorStore.id, batch.id); + +while (batch.status === 'in_progress') { + await new Promise(r => setTimeout(r, 2000)); + batch = await openai.beta.vectorStores.fileBatches.retrieve(vectorStore.id, batch.id); +} +``` + +### 4. Create Assistant + +```typescript +const assistant = await openai.beta.assistants.create({ + name: "Knowledge Base Assistant", + instructions: "Answer questions using the file search tool. Always cite your sources.", + tools: [{ type: "file_search" }], + tool_resources: { + file_search: { + vector_store_ids: [vectorStore.id], + }, + }, + model: "gpt-4o", +}); +``` + +--- + +## Supported File Formats + +- `.pdf` - PDFs (most common) +- `.docx` - Word documents +- `.md`, `.txt` - Plain text +- `.html` - HTML documents +- `.json` - JSON data +- `.py`, `.js`, `.ts`, `.cpp`, `.java` - Code files + +**Size Limits**: +- **Per file**: 512 MB +- **Total per vector store**: Limited by pricing ($0.10/GB/day) + +--- + +## Chunking Strategy + +OpenAI automatically chunks documents using: +- **Max chunk size**: ~800 tokens (configurable internally) +- **Overlap**: Ensures context continuity +- **Hierarchy**: Preserves document structure (headers, sections) + +### Optimize for Better Results + +**Document Structure**: +```markdown +# Main Topic + +## Subtopic 1 +Content here... + +## Subtopic 2 +Content here... +``` + +**Clear Sections**: Use headers to organize content +**Concise Paragraphs**: Avoid very long paragraphs (500+ words) +**Self-Contained**: Each section should make sense independently + +--- + +## Improving Search Quality + +### 1. Better Instructions + +```typescript +const assistant = await openai.beta.assistants.create({ + instructions: `You are a support assistant. When answering: +1. Use file_search to find relevant information +2. Synthesize information from multiple sources +3. Always provide citations with file names +4. If information isn't found, say so clearly +5. Don't make up information not in the documents`, + tools: [{ type: "file_search" }], + // ... +}); +``` + +### 2. Query Refinement + +Encourage users to be specific: +- ❌ "How do I install?" +- ✅ "How do I install the product on Windows 10?" + +### 3. Multi-Document Answers + +File Search automatically retrieves from multiple documents and combines information. + +--- + +## Citations + +### Accessing Citations + +```typescript +const messages = await openai.beta.threads.messages.list(thread.id); +const response = messages.data[0]; + +for (const content of response.content) { + if (content.type === 'text') { + console.log('Answer:', content.text.value); + + // Citations + if (content.text.annotations) { + for (const annotation of content.text.annotations) { + if (annotation.type === 'file_citation') { + console.log('Source:', annotation.file_citation.file_id); + console.log('Quote:', annotation.file_citation.quote); + } + } + } + } +} +``` + +### Displaying Citations + +```typescript +let answer = response.content[0].text.value; + +// Replace citation markers with clickable links +for (const annotation of response.content[0].text.annotations) { + if (annotation.type === 'file_citation') { + const citation = `[${annotation.text}](source: ${annotation.file_citation.file_id})`; + answer = answer.replace(annotation.text, citation); + } +} + +console.log(answer); +``` + +--- + +## Cost Management + +### Pricing Structure + +- **Storage**: $0.10/GB/day +- **Free tier**: First 1GB +- **Example**: 5GB = $0.40/day = $12/month + +### Optimization Strategies + +1. **Auto-Expiration**: +```typescript +const vectorStore = await openai.beta.vectorStores.create({ + expires_after: { + anchor: "last_active_at", + days: 7, // Delete after 7 days of inactivity + }, +}); +``` + +2. **Cleanup Old Stores**: +```typescript +async function cleanupOldVectorStores() { + const stores = await openai.beta.vectorStores.list({ limit: 100 }); + + for (const store of stores.data) { + const ageDays = (Date.now() / 1000 - store.created_at) / (60 * 60 * 24); + + if (ageDays > 30) { + await openai.beta.vectorStores.del(store.id); + } + } +} +``` + +3. **Monitor Usage**: +```typescript +const store = await openai.beta.vectorStores.retrieve(vectorStoreId); +const sizeGB = store.usage_bytes / (1024 * 1024 * 1024); +const costPerDay = Math.max(0, (sizeGB - 1) * 0.10); +console.log(`Daily cost: $${costPerDay.toFixed(4)}`); +``` + +--- + +## Advanced Patterns + +### Pattern: Multi-Tenant Knowledge Bases + +```typescript +// Separate vector store per tenant +const tenantStore = await openai.beta.vectorStores.create({ + name: `Tenant ${tenantId} KB`, + metadata: { tenant_id: tenantId }, +}); + +// Or: Single store with namespace simulation via file metadata +await openai.files.create({ + file: fs.createReadStream("doc.pdf"), + purpose: "assistants", + metadata: { tenant_id: tenantId }, // Coming soon +}); +``` + +### Pattern: Versioned Documentation + +```typescript +// Version 1.0 +const v1Store = await openai.beta.vectorStores.create({ + name: "Docs v1.0", + metadata: { version: "1.0" }, +}); + +// Version 2.0 +const v2Store = await openai.beta.vectorStores.create({ + name: "Docs v2.0", + metadata: { version: "2.0" }, +}); + +// Switch based on user preference +const storeId = userVersion === "1.0" ? v1Store.id : v2Store.id; +``` + +### Pattern: Hybrid Search (File Search + Code Interpreter) + +```typescript +const assistant = await openai.beta.assistants.create({ + tools: [ + { type: "file_search" }, + { type: "code_interpreter" }, + ], + tool_resources: { + file_search: { + vector_store_ids: [docsVectorStoreId], + }, + }, +}); + +// Assistant can search docs AND analyze attached data files +await openai.beta.threads.messages.create(thread.id, { + content: "Compare this sales data against the targets in our planning docs", + attachments: [{ + file_id: salesDataFileId, + tools: [{ type: "code_interpreter" }], + }], +}); +``` + +--- + +## Troubleshooting + +### No Results Found + +**Causes**: +- Vector store not fully indexed +- Poor query formulation +- Documents lack relevant content + +**Solutions**: +- Wait for `status: "completed"` +- Refine query to be more specific +- Check document quality and structure + +### Irrelevant Results + +**Causes**: +- Poor document structure +- Too much noise in documents +- Vague queries + +**Solutions**: +- Add clear section headers +- Remove boilerplate/repetitive content +- Improve query specificity + +### High Costs + +**Causes**: +- Too many vector stores +- Large files that don't expire +- Duplicate content + +**Solutions**: +- Set auto-expiration +- Deduplicate documents +- Delete unused stores + +--- + +## Best Practices + +1. **Structure documents** with clear headers and sections +2. **Wait for indexing** before using vector store +3. **Set auto-expiration** to manage costs +4. **Monitor storage** regularly +5. **Provide citations** in responses +6. **Refine queries** for better results +7. **Clean up** old vector stores + +--- + +**Last Updated**: 2025-10-25 diff --git a/references/migration-from-v1.md b/references/migration-from-v1.md new file mode 100644 index 0000000..fd4030a --- /dev/null +++ b/references/migration-from-v1.md @@ -0,0 +1,188 @@ +# Migration from Assistants API v1 to v2 + +**v1 Deprecated**: December 18, 2024 (no longer accessible) +**v2 Status**: Production (Deprecated H1 2026 in favor of Responses API) + +--- + +## Breaking Changes + +### 1. Retrieval Tool → File Search + +**v1:** +```typescript +{ + tools: [{ type: "retrieval" }], + file_ids: ["file_abc123", "file_def456"] +} +``` + +**v2:** +```typescript +{ + tools: [{ type: "file_search" }], + tool_resources: { + file_search: { + vector_store_ids: ["vs_abc123"] + } + } +} +``` + +**Action**: Create vector stores and migrate files. + +### 2. File Attachments + +**v1**: Files attached at assistant level +**v2**: Files attached at message level + +**v1:** +```typescript +const assistant = await openai.beta.assistants.create({ + file_ids: ["file_abc123"], +}); +``` + +**v2:** +```typescript +await openai.beta.threads.messages.create(thread.id, { + content: "...", + attachments: [{ + file_id: "file_abc123", + tools: [{ type: "code_interpreter" }] + }], +}); +``` + +### 3. Instructions Character Limit + +- **v1**: 32,000 characters +- **v2**: 256,000 characters (8x increase) + +--- + +## Migration Steps + +### Step 1: Create Vector Stores + +```typescript +// Old v1 approach +const assistant = await openai.beta.assistants.create({ + tools: [{ type: "retrieval" }], + file_ids: fileIds, // Direct attachment +}); + +// New v2 approach +const vectorStore = await openai.beta.vectorStores.create({ + name: "Knowledge Base", +}); + +await openai.beta.vectorStores.fileBatches.create(vectorStore.id, { + file_ids: fileIds, +}); + +const assistant = await openai.beta.assistants.create({ + tools: [{ type: "file_search" }], + tool_resources: { + file_search: { + vector_store_ids: [vectorStore.id], + }, + }, +}); +``` + +### Step 2: Update File Attachments + +```typescript +// Move file attachments from assistant to messages +await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "Analyze this file", + attachments: [{ + file_id: "file_abc123", + tools: [{ type: "code_interpreter" }], + }], +}); +``` + +### Step 3: Test Thoroughly + +- Verify file search returns expected results +- Check Code Interpreter file handling +- Test streaming if used +- Validate function calling patterns + +--- + +## New v2 Features + +### 1. Massive File Capacity + +- **v1**: ~20 files per assistant +- **v2**: 10,000 files per assistant (500x increase) + +### 2. Better Search Performance + +- Vector + keyword search +- Parallel query processing +- Advanced reranking + +### 3. Auto-Expiration + +```typescript +const vectorStore = await openai.beta.vectorStores.create({ + expires_after: { + anchor: "last_active_at", + days: 30, + }, +}); +``` + +### 4. Batch File Operations + +```typescript +const batch = await openai.beta.vectorStores.fileBatches.create(vectorStoreId, { + file_ids: ["file_1", "file_2", "file_3"], +}); +``` + +--- + +## Cost Implications + +### v1 (Deprecated) +- No separate storage costs for retrieval + +### v2 +- **Storage**: $0.10/GB/day for vector stores +- **Free tier**: First 1GB +- **Optimization**: Use auto-expiration + +--- + +## Recommended Path Forward + +**For existing v1 applications:** +1. Migrate to v2 immediately (v1 no longer works) +2. Plan migration to Responses API (v2 sunset in H1 2026) + +**For new applications:** +- ✅ Use [Responses API](../../openai-responses/SKILL.md) +- ❌ Don't use Assistants API (being deprecated) + +--- + +## Migration Checklist + +- [ ] Update to openai SDK 6.7.0+ +- [ ] Create vector stores for file search +- [ ] Migrate file attachments to message level +- [ ] Test file search results +- [ ] Update to `file_search` from `retrieval` +- [ ] Implement vector store cleanup +- [ ] Monitor storage costs +- [ ] Plan migration to Responses API + +--- + +**Last Updated**: 2025-10-25 diff --git a/references/thread-lifecycle.md b/references/thread-lifecycle.md new file mode 100644 index 0000000..86f6d74 --- /dev/null +++ b/references/thread-lifecycle.md @@ -0,0 +1,271 @@ +# Thread Lifecycle Management + +Complete guide to managing threads effectively to avoid errors and optimize performance. + +--- + +## Thread States + +A thread progresses through these states based on run activity: + +``` +Idle (no active runs) + ↓ +Active (run in progress) + ↓ +Requires Action (function calling) + ↓ +Completed / Failed / Cancelled + ↓ +Idle (ready for next run) +``` + +--- + +## Common Patterns + +### Pattern 1: One Thread Per User + +**Use Case**: Chatbots, support assistants + +```typescript +// In-memory cache (use database in production) +const userThreads = new Map(); + +async function getOrCreateUserThread(userId: string): Promise { + let threadId = userThreads.get(userId); + + if (!threadId) { + const thread = await openai.beta.threads.create({ + metadata: { user_id: userId }, + }); + threadId = thread.id; + userThreads.set(userId, threadId); + } + + return threadId; +} +``` + +**Benefits**: +- Conversation continuity +- Automatic history management +- Simple architecture + +**Drawbacks**: +- Long threads consume memory +- 100k message limit eventually + +### Pattern 2: Session-Based Threads + +**Use Case**: Temporary conversations + +```typescript +async function createSessionThread(userId: string, sessionId: string) { + return await openai.beta.threads.create({ + metadata: { + user_id: userId, + session_id: sessionId, + expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }, + }); +} +``` + +**Benefits**: +- Clear boundaries +- Easy cleanup +- Fresh context + +### Pattern 3: Topic-Based Threads + +**Use Case**: Multi-topic conversations + +```typescript +async function getTopicThread(userId: string, topic: string) { + const key = `${userId}:${topic}`; + // Separate threads for different topics + return await getOrCreateThread(key); +} +``` + +--- + +## Active Run Management + +### Check for Active Runs + +```typescript +async function hasActiveRun(threadId: string): Promise { + const runs = await openai.beta.threads.runs.list(threadId, { + limit: 1, + order: 'desc', + }); + + const latestRun = runs.data[0]; + return latestRun && ['queued', 'in_progress', 'cancelling'].includes(latestRun.status); +} +``` + +### Safe Run Creation + +```typescript +async function createRunSafely( + threadId: string, + assistantId: string, + cancelIfActive = true +) { + // Check for active runs + const runs = await openai.beta.threads.runs.list(threadId, { + limit: 1, + order: 'desc', + }); + + const latestRun = runs.data[0]; + if (latestRun && ['queued', 'in_progress'].includes(latestRun.status)) { + if (cancelIfActive) { + await openai.beta.threads.runs.cancel(threadId, latestRun.id); + // Wait for cancellation + while (latestRun.status !== 'cancelled') { + await new Promise(r => setTimeout(r, 500)); + latestRun = await openai.beta.threads.runs.retrieve(threadId, latestRun.id); + } + } else { + throw new Error('Thread has active run'); + } + } + + return await openai.beta.threads.runs.create(threadId, { + assistant_id: assistantId, + }); +} +``` + +--- + +## Cleanup Strategies + +### Time-Based Cleanup + +```typescript +async function cleanupOldThreads(maxAgeHours: number = 24) { + // Query your database for old threads + const oldThreads = await db.getThreadsOlderThan(maxAgeHours); + + for (const threadId of oldThreads) { + await openai.beta.threads.del(threadId); + await db.deleteThreadRecord(threadId); + } +} +``` + +### Message Count-Based + +```typescript +async function archiveIfTooLong(threadId: string, maxMessages: number = 1000) { + const messages = await openai.beta.threads.messages.list(threadId); + + if (messages.data.length >= maxMessages) { + // Archive to database + await db.archiveThread(threadId, messages.data); + + // Create new thread + return await openai.beta.threads.create({ + metadata: { previous_thread: threadId }, + }); + } + + return threadId; +} +``` + +### Safe Deletion + +```typescript +async function safeDeleteThread(threadId: string) { + // Cancel all active runs first + const runs = await openai.beta.threads.runs.list(threadId); + + for (const run of runs.data) { + if (['queued', 'in_progress'].includes(run.status)) { + await openai.beta.threads.runs.cancel(threadId, run.id); + } + } + + // Wait a moment for cancellations + await new Promise(r => setTimeout(r, 1000)); + + // Delete thread + await openai.beta.threads.del(threadId); +} +``` + +--- + +## Error Handling + +### Concurrent Run Prevention + +```typescript +class ThreadManager { + private locks = new Map>(); + + async executeRun(threadId: string, assistantId: string) { + // Wait if another run is in progress + if (this.locks.has(threadId)) { + await this.locks.get(threadId); + } + + // Create lock + const runPromise = this._runAssistant(threadId, assistantId); + this.locks.set(threadId, runPromise); + + try { + return await runPromise; + } finally { + this.locks.delete(threadId); + } + } + + private async _runAssistant(threadId: string, assistantId: string) { + // Run logic here + } +} +``` + +--- + +## Best Practices + +1. **Always check for active runs** before creating new ones +2. **Use metadata** to track ownership and expiration +3. **Implement cleanup** to manage costs +4. **Set reasonable limits** (message count, age) +5. **Handle errors gracefully** (active run conflicts) +6. **Archive old conversations** before deletion +7. **Use locks** for concurrent access + +--- + +## Monitoring + +```typescript +async function getThreadStats(threadId: string) { + const thread = await openai.beta.threads.retrieve(threadId); + const messages = await openai.beta.threads.messages.list(threadId); + const runs = await openai.beta.threads.runs.list(threadId); + + return { + threadId: thread.id, + createdAt: new Date(thread.created_at * 1000), + messageCount: messages.data.length, + runCount: runs.data.length, + lastRun: runs.data[0], + metadata: thread.metadata, + }; +} +``` + +--- + +**Last Updated**: 2025-10-25 diff --git a/references/top-errors.md b/references/top-errors.md new file mode 100644 index 0000000..e556679 --- /dev/null +++ b/references/top-errors.md @@ -0,0 +1,446 @@ +# Top 12 Errors - OpenAI Assistants API + +**Last Updated**: 2025-10-25 + +This document catalogs the most common errors encountered when working with the Assistants API v2 and their solutions. + +--- + +## 1. Thread Already Has Active Run + +**Error Message**: +``` +Error: 400 Can't add messages to thread_xxx while a run run_xxx is active. +``` + +**Cause**: Trying to create a new run or add messages while another run is still processing (status: `queued`, `in_progress`, or `cancelling`). + +**Solution**: +```typescript +async function ensureNoActiveRun(threadId: string) { + const runs = await openai.beta.threads.runs.list(threadId, { + limit: 1, + order: 'desc', + }); + + const latestRun = runs.data[0]; + if (latestRun && ['queued', 'in_progress', 'cancelling'].includes(latestRun.status)) { + // Wait for completion or cancel + await openai.beta.threads.runs.cancel(threadId, latestRun.id); + + // Poll until cancelled + let run = latestRun; + while (run.status !== 'cancelled') { + await new Promise(resolve => setTimeout(resolve, 500)); + run = await openai.beta.threads.runs.retrieve(threadId, run.id); + } + } +} +``` + +**Prevention**: Always check for active runs before creating new ones. + +**Source**: [OpenAI Community](https://community.openai.com/t/error-running-thread-already-has-an-active-run/782118) + +--- + +## 2. Run Polling Timeout + +**Error**: Run never completes within reasonable polling window (300+ seconds). + +**Cause**: Long-running tasks (complex code execution, large file processing) exceed expected completion time. + +**Solution**: +```typescript +async function pollWithTimeout(threadId: string, runId: string, maxSeconds = 300) { + const startTime = Date.now(); + + while (true) { + const run = await openai.beta.threads.runs.retrieve(threadId, runId); + + if (!['queued', 'in_progress'].includes(run.status)) { + return run; + } + + const elapsed = (Date.now() - startTime) / 1000; + if (elapsed > maxSeconds) { + await openai.beta.threads.runs.cancel(threadId, runId); + throw new Error(`Run exceeded timeout of ${maxSeconds}s`); + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } +} +``` + +**Prevention**: Set appropriate timeouts and use streaming for better UX. + +--- + +## 3. Vector Store Indexing Delay + +**Error**: File search returns no results despite files being uploaded. + +**Cause**: Using vector store before indexing completes (async process). + +**Solution**: +```typescript +async function waitForVectorStore(vectorStoreId: string) { + let store = await openai.beta.vectorStores.retrieve(vectorStoreId); + + while (store.status === 'in_progress') { + await new Promise(resolve => setTimeout(resolve, 2000)); + store = await openai.beta.vectorStores.retrieve(vectorStoreId); + console.log(`Indexing: ${store.file_counts.completed}/${store.file_counts.total}`); + } + + if (store.status === 'failed') { + throw new Error('Vector store indexing failed'); + } + + return store; // status: 'completed' +} +``` + +**Prevention**: Always wait for `status: "completed"` before using vector store with assistants. + +**Source**: [OpenAI Community](https://community.openai.com/t/assistants-api-file-search-and-vector-stores/863944) + +--- + +## 4. File Search Relevance Issues + +**Error**: File search returns irrelevant or incomplete results. + +**Cause**: Poor document chunking, lack of context, or query optimization needed. + +**Solution**: +- **Better instructions**: Guide assistant on how to use file search +- **Structured documents**: Use clear headers, sections, and formatting +- **Metadata**: Add descriptive metadata to files (coming soon) +- **Query refinement**: Encourage users to be specific + +```typescript +const assistant = await openai.beta.assistants.create({ + instructions: `You are a support assistant. When answering: + 1. Use file_search to find relevant documentation + 2. Quote specific sections with citations + 3. If information isn't found, say so clearly + 4. Provide context around the answer`, + tools: [{ type: "file_search" }], + // ... +}); +``` + +**Prevention**: Structure documents well and provide clear assistant instructions. + +--- + +## 5. Code Interpreter File Output Not Found + +**Error**: `image_file.file_id` referenced but file doesn't exist or can't be downloaded. + +**Cause**: Files generated by Code Interpreter are temporary and may be cleaned up before retrieval. + +**Solution**: +```typescript +// Retrieve and save immediately after run completes +const messages = await openai.beta.threads.messages.list(threadId); +const responseMessage = messages.data[0]; + +for (const content of responseMessage.content) { + if (content.type === 'image_file') { + try { + const fileData = await openai.files.content(content.image_file.file_id); + const buffer = Buffer.from(await fileData.arrayBuffer()); + fs.writeFileSync(`output_${content.image_file.file_id}.png`, buffer); + } catch (error) { + console.error('File no longer available:', error); + } + } +} +``` + +**Prevention**: Download generated files immediately after run completion. + +**Source**: [Medium - Code Interpreter Tutorial](https://tmmtt.medium.com/openai-assistant-api-with-code-interpreter-e7f382bff83e) + +--- + +## 6. Thread Message Limit Exceeded + +**Error**: `400 Thread has exceeded the maximum number of messages (100,000)`. + +**Cause**: Very long conversations hitting the 100k message limit. + +**Solution**: +```typescript +async function archiveAndStartNewThread(oldThreadId: string, userId: string) { + // Get conversation summary + const messages = await openai.beta.threads.messages.list(oldThreadId, { + limit: 50, + }); + + // Save to database + await db.archiveThread(oldThreadId, messages.data); + + // Create new thread + const newThread = await openai.beta.threads.create({ + metadata: { + user_id: userId, + previous_thread: oldThreadId, + }, + }); + + return newThread.id; +} +``` + +**Prevention**: Archive old threads and create new ones periodically. + +--- + +## 7. Function Calling Timeout + +**Error**: Run expires (status: `expired`) while waiting for tool outputs. + +**Cause**: Tool execution takes too long (max 10 minutes for run). + +**Solution**: +```typescript +if (run.status === 'requires_action') { + const toolCalls = run.required_action.submit_tool_outputs.tool_calls; + const toolOutputs = []; + + for (const toolCall of toolCalls) { + try { + // Add timeout to function execution + const output = await Promise.race([ + executeFunction(toolCall.function.name, JSON.parse(toolCall.function.arguments)), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Function timeout')), 30000) + ), + ]); + + toolOutputs.push({ + tool_call_id: toolCall.id, + output: JSON.stringify(output), + }); + } catch (error) { + // Return error as output + toolOutputs.push({ + tool_call_id: toolCall.id, + output: JSON.stringify({ error: error.message }), + }); + } + } + + await openai.beta.threads.runs.submitToolOutputs(threadId, run.id, { + tool_outputs: toolOutputs, + }); +} +``` + +**Prevention**: Implement timeouts on function execution and return errors gracefully. + +--- + +## 8. Streaming Run Interruption + +**Error**: Stream connection closes unexpectedly or events stop arriving. + +**Cause**: Network issues, server errors, or run failures. + +**Solution**: +```typescript +async function streamWithRetry(threadId: string, assistantId: string) { + try { + const stream = await openai.beta.threads.runs.stream(threadId, { + assistant_id: assistantId, + }); + + for await (const event of stream) { + // Handle events + if (event.event === 'error') { + throw new Error('Stream error'); + } + } + } catch (error) { + console.error('Stream interrupted:', error); + + // Fall back to polling + const runs = await openai.beta.threads.runs.list(threadId, { + limit: 1, + order: 'desc', + }); + + const run = runs.data[0]; + return pollRunCompletion(threadId, run.id); + } +} +``` + +**Prevention**: Implement fallback to polling if streaming fails. + +**Source**: [OpenAI Community](https://community.openai.com/t/streaming-stopped-at-thread-run-requires-action-when-handling-openai-assistants-function-calling/943674) + +--- + +## 9. Vector Store Quota Limits + +**Error**: `429 Rate limit reached for vector store operations`. + +**Cause**: Too many vector store operations or storage exceeded. + +**Solution**: +- **Monitor storage**: Check `usage_bytes` regularly +- **Delete unused stores**: Clean up old vector stores +- **Batch operations**: Use batch file uploads instead of individual uploads + +```typescript +async function cleanupOldVectorStores(keepDays = 30) { + const stores = await openai.beta.vectorStores.list({ limit: 100 }); + + for (const store of stores.data) { + const ageSeconds = Date.now() / 1000 - store.created_at; + const ageDays = ageSeconds / (60 * 60 * 24); + + if (ageDays > keepDays) { + await openai.beta.vectorStores.del(store.id); + console.log(`Deleted vector store: ${store.id}`); + } + } +} +``` + +**Prevention**: Set auto-expiration on vector stores and monitor costs. + +--- + +## 10. File Upload Format Incompatibility + +**Error**: `400 Unsupported file type for this tool`. + +**Cause**: Uploading file format not supported by the tool. + +**Solution**: +```typescript +const SUPPORTED_FORMATS = { + code_interpreter: [ + '.c', '.cpp', '.csv', '.docx', '.html', '.java', '.json', '.md', + '.pdf', '.php', '.pptx', '.py', '.rb', '.tex', '.txt', '.css', + '.jpeg', '.jpg', '.js', '.gif', '.png', '.tar', '.ts', '.xlsx', '.xml', '.zip' + ], + file_search: [ + '.c', '.cpp', '.docx', '.html', '.java', '.json', '.md', + '.pdf', '.php', '.pptx', '.py', '.rb', '.tex', '.txt', '.css', '.js', '.ts', '.go' + ], +}; + +function validateFileFormat(filename: string, tool: 'code_interpreter' | 'file_search') { + const ext = filename.substring(filename.lastIndexOf('.')).toLowerCase(); + + if (!SUPPORTED_FORMATS[tool].includes(ext)) { + throw new Error(`Unsupported file format for ${tool}: ${ext}`); + } +} + +// Validate before upload +validateFileFormat('data.csv', 'code_interpreter'); // OK +validateFileFormat('video.mp4', 'file_search'); // Throws error +``` + +**Prevention**: Validate file formats before uploading. + +--- + +## 11. Assistant Instructions Token Limit + +**Error**: `400 Instructions exceed maximum length`. + +**Cause**: Instructions field exceeds 256,000 characters (v2 limit). + +**Solution**: +- **Use file search**: Put long instructions in documents +- **Concise instructions**: Be clear and brief +- **System messages**: Use thread-level messages for context + +```typescript +// ❌ Bad: Very long instructions +const assistant = await openai.beta.assistants.create({ + instructions: "..." // 300k characters +}); + +// ✅ Good: Concise instructions + file search +const assistant = await openai.beta.assistants.create({ + instructions: "You are a support assistant. Use file_search to find answers in the knowledge base.", + tools: [{ type: "file_search" }], + tool_resources: { + file_search: { + vector_store_ids: [vectorStoreId], // Long content here + }, + }, +}); +``` + +**Prevention**: Keep instructions under 256k characters; use file search for knowledge. + +--- + +## 12. Thread Deletion While Run Active + +**Error**: `400 Cannot delete thread while run is active`. + +**Cause**: Attempting to delete a thread that has an active run. + +**Solution**: +```typescript +async function safeDeleteThread(threadId: string) { + // Cancel active runs first + const runs = await openai.beta.threads.runs.list(threadId); + + for (const run of runs.data) { + if (['queued', 'in_progress'].includes(run.status)) { + await openai.beta.threads.runs.cancel(threadId, run.id); + + // Wait for cancellation + let runStatus = run; + while (runStatus.status !== 'cancelled') { + await new Promise(resolve => setTimeout(resolve, 500)); + runStatus = await openai.beta.threads.runs.retrieve(threadId, run.id); + } + } + } + + // Now safe to delete + await openai.beta.threads.del(threadId); +} +``` + +**Prevention**: Cancel all active runs before deleting threads. + +--- + +## Quick Reference + +| Error | Quick Fix | +|-------|-----------| +| Thread has active run | Cancel or wait for run completion | +| Polling timeout | Set timeout and cancel long runs | +| Vector store not ready | Wait for `status: "completed"` | +| File search no results | Check indexing complete, improve queries | +| Code Interpreter file lost | Download immediately after run | +| 100k message limit | Archive old threads, start new ones | +| Function timeout | Add timeouts to function execution | +| Stream interrupted | Fall back to polling | +| Vector store quota | Clean up old stores, use batch uploads | +| Unsupported file format | Validate file extensions before upload | +| Instructions too long | Use file search for knowledge | +| Can't delete thread | Cancel active runs first | + +--- + +**Additional Resources**: +- [OpenAI Assistants API Docs](https://platform.openai.com/docs/assistants) +- [OpenAI Community Forum](https://community.openai.com/c/api/assistants-api/49) +- [API Reference](https://platform.openai.com/docs/api-reference/assistants) diff --git a/references/vector-stores.md b/references/vector-stores.md new file mode 100644 index 0000000..06d1c5b --- /dev/null +++ b/references/vector-stores.md @@ -0,0 +1,422 @@ +# Vector Stores - Complete Reference + +In-depth guide to OpenAI's Vector Stores for the Assistants API. + +--- + +## Overview + +Vector Stores provide scalable semantic search infrastructure for the file_search tool: +- **Capacity**: Up to 10,000 files per assistant +- **Automatic**: Chunking, embedding, indexing +- **Search**: Vector + keyword hybrid with reranking +- **Pricing**: $0.10/GB/day (first 1GB free) + +--- + +## Creating Vector Stores + +### Basic Creation + +```typescript +const vectorStore = await openai.beta.vectorStores.create({ + name: "Company Knowledge Base", +}); +``` + +### With Auto-Expiration + +```typescript +const vectorStore = await openai.beta.vectorStores.create({ + name: "Temporary KB", + expires_after: { + anchor: "last_active_at", + days: 7, + }, +}); +``` + +**Anchors**: +- `last_active_at`: Expires N days after last use (recommended) +- `created_at`: Expires N days after creation (not yet available) + +### With Metadata + +```typescript +const vectorStore = await openai.beta.vectorStores.create({ + name: "Q4 2025 Documentation", + metadata: { + department: "sales", + quarter: "Q4-2025", + version: "1.0", + }, +}); +``` + +--- + +## Adding Files + +### Single File Upload + +```typescript +// 1. Upload file to OpenAI +const file = await openai.files.create({ + file: fs.createReadStream("document.pdf"), + purpose: "assistants", +}); + +// 2. Add to vector store +await openai.beta.vectorStores.files.create(vectorStore.id, { + file_id: file.id, +}); +``` + +### Batch Upload (Recommended) + +```typescript +// Upload multiple files +const files = await Promise.all([ + openai.files.create({ file: fs.createReadStream("doc1.pdf"), purpose: "assistants" }), + openai.files.create({ file: fs.createReadStream("doc2.md"), purpose: "assistants" }), + openai.files.create({ file: fs.createReadStream("doc3.docx"), purpose: "assistants" }), +]); + +// Batch add to vector store +const batch = await openai.beta.vectorStores.fileBatches.create(vectorStore.id, { + file_ids: files.map(f => f.id), +}); + +// Monitor progress +let batchStatus = batch; +while (batchStatus.status === 'in_progress') { + await new Promise(r => setTimeout(r, 1000)); + batchStatus = await openai.beta.vectorStores.fileBatches.retrieve( + vectorStore.id, + batch.id + ); + console.log(`${batchStatus.file_counts.completed}/${batchStatus.file_counts.total}`); +} +``` + +**Benefits of Batch Upload**: +- Faster processing (parallel indexing) +- Single operation to track +- Better error handling + +--- + +## Vector Store States + +| State | Description | +|-------|-------------| +| `in_progress` | Files being indexed | +| `completed` | All files indexed successfully | +| `failed` | Indexing failed | +| `expired` | Auto-expiration triggered | + +**Important**: Wait for `completed` before using with assistants. + +--- + +## File Management + +### List Files in Vector Store + +```typescript +const files = await openai.beta.vectorStores.files.list(vectorStore.id, { + limit: 100, +}); + +for (const file of files.data) { + console.log(`${file.id}: ${file.status}`); +} +``` + +### Remove File from Vector Store + +```typescript +await openai.beta.vectorStores.files.del(vectorStore.id, fileId); +``` + +**Note**: This removes the file from the vector store but doesn't delete the file from OpenAI's storage. + +### Check File Status + +```typescript +const file = await openai.beta.vectorStores.files.retrieve(vectorStore.id, fileId); + +console.log(file.status); // "in_progress", "completed", "failed" + +if (file.status === 'failed') { + console.error(file.last_error); +} +``` + +--- + +## Pricing & Cost Management + +### Pricing Structure + +- **Storage**: $0.10 per GB per day +- **Free tier**: First 1GB +- **Calculation**: Total vector store size (not original file size) + +**Example Costs**: +| Original Files | Vector Store Size | Daily Cost | Monthly Cost | +|----------------|-------------------|------------|--------------| +| 500 MB | 0.5 GB | $0.00 | $0.00 (free tier) | +| 2 GB | 2 GB | $0.10 | $3.00 | +| 10 GB | 10 GB | $0.90 | $27.00 | +| 50 GB | 50 GB | $4.90 | $147.00 | + +### Monitor Usage + +```typescript +const store = await openai.beta.vectorStores.retrieve(vectorStoreId); + +const sizeGB = store.usage_bytes / (1024 * 1024 * 1024); +const costPerDay = Math.max(0, (sizeGB - 1) * 0.10); +const costPerMonth = costPerDay * 30; + +console.log(`Storage: ${sizeGB.toFixed(2)} GB`); +console.log(`Cost: $${costPerDay.toFixed(4)}/day ($${costPerMonth.toFixed(2)}/month)`); +``` + +### Cost Optimization + +**1. Auto-Expiration**: +```typescript +expires_after: { + anchor: "last_active_at", + days: 30, +} +``` + +**2. Regular Cleanup**: +```typescript +async function cleanupUnusedVectorStores() { + const stores = await openai.beta.vectorStores.list({ limit: 100 }); + + for (const store of stores.data) { + const ageDays = (Date.now() / 1000 - store.created_at) / (60 * 60 * 24); + + if (ageDays > 90) { // 90 days old + await openai.beta.vectorStores.del(store.id); + console.log(`Deleted: ${store.name}`); + } + } +} +``` + +**3. Deduplicate Content**: +- Remove duplicate files before upload +- Combine similar documents +- Archive old versions + +--- + +## Using with Assistants + +### Attach to Assistant + +```typescript +const assistant = await openai.beta.assistants.create({ + name: "Support Bot", + tools: [{ type: "file_search" }], + tool_resources: { + file_search: { + vector_store_ids: [vectorStore.id], + }, + }, + model: "gpt-4o", +}); +``` + +### Multiple Vector Stores + +```typescript +// Combine multiple knowledge bases +tool_resources: { + file_search: { + vector_store_ids: [generalKBId, productDocsId, policyDocsId], + }, +} +``` + +**Limit**: Maximum of 1 vector store per assistant in current API (subject to change). + +--- + +## Advanced Operations + +### Update Metadata + +```typescript +const updated = await openai.beta.vectorStores.update(vectorStoreId, { + name: "Updated Name", + metadata: { + version: "2.0", + last_updated: new Date().toISOString(), + }, +}); +``` + +### Retrieve Vector Store Details + +```typescript +const store = await openai.beta.vectorStores.retrieve(vectorStoreId); + +console.log({ + id: store.id, + name: store.name, + status: store.status, + usage_bytes: store.usage_bytes, + file_counts: store.file_counts, + created_at: new Date(store.created_at * 1000), + expires_at: store.expires_at ? new Date(store.expires_at * 1000) : null, + metadata: store.metadata, +}); +``` + +### List All Vector Stores + +```typescript +const stores = await openai.beta.vectorStores.list({ + limit: 20, + order: "desc", +}); + +for (const store of stores.data) { + console.log(`${store.name}: ${store.file_counts.completed} files`); +} +``` + +--- + +## Best Practices + +### 1. Pre-Process Documents + +- Remove headers/footers +- Clean formatting +- Extract text from images (OCR separately) +- Organize with clear structure + +### 2. Monitor Indexing + +```typescript +async function waitForIndexing(vectorStoreId: string, batchId: string) { + let batch; + const startTime = Date.now(); + + do { + batch = await openai.beta.vectorStores.fileBatches.retrieve(vectorStoreId, batchId); + + if (batch.status === 'failed') { + throw new Error('Batch indexing failed'); + } + + console.log(`Progress: ${batch.file_counts.completed}/${batch.file_counts.total}`); + + await new Promise(r => setTimeout(r, 2000)); + + // Timeout after 10 minutes + if (Date.now() - startTime > 600000) { + throw new Error('Indexing timeout'); + } + } while (batch.status === 'in_progress'); + + return batch; +} +``` + +### 3. Set Reasonable Expiration + +```typescript +// For temporary projects +expires_after: { anchor: "last_active_at", days: 7 } + +// For active knowledge bases +expires_after: { anchor: "last_active_at", days: 90 } + +// For permanent KB (no expiration) +// Don't set expires_after +``` + +### 4. Tag with Metadata + +```typescript +metadata: { + project: "project-alpha", + environment: "production", + version: "1.0", + owner: "team@company.com", +} +``` + +--- + +## Troubleshooting + +### Files Not Indexing + +**Check file status**: +```typescript +const file = await openai.beta.vectorStores.files.retrieve(vectorStoreId, fileId); + +if (file.status === 'failed') { + console.error(file.last_error); +} +``` + +**Common causes**: +- Unsupported file format +- Corrupted file +- File too large (>512 MB) + +### Vector Store Shows `failed` Status + +**Check batch details**: +```typescript +const batch = await openai.beta.vectorStores.fileBatches.retrieve(vectorStoreId, batchId); +console.log(batch.file_counts); // Check failed count +``` + +**Solutions**: +- Remove failed files +- Re-upload with correct format +- Check error messages + +### High Storage Costs + +**Audit vector stores**: +```typescript +const stores = await openai.beta.vectorStores.list({ limit: 100 }); +let totalGB = 0; + +for (const store of stores.data) { + const sizeGB = store.usage_bytes / (1024 * 1024 * 1024); + totalGB += sizeGB; + console.log(`${store.name}: ${sizeGB.toFixed(2)} GB`); +} + +console.log(`Total: ${totalGB.toFixed(2)} GB = $${((totalGB - 1) * 0.10).toFixed(2)}/day`); +``` + +--- + +## Limits + +| Resource | Limit | +|----------|-------| +| Files per vector store | 10,000 | +| Vector stores per account | Not documented | +| File size | 512 MB | +| Storage (billable) | Unlimited (pay per GB) | +| Indexing time | Varies by size | + +--- + +**Last Updated**: 2025-10-25 diff --git a/scripts/check-versions.sh b/scripts/check-versions.sh new file mode 100755 index 0000000..cad6c6b --- /dev/null +++ b/scripts/check-versions.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# OpenAI Assistants API - Package Version Checker +# Verifies that the correct package versions are installed + +set -e + +echo "🔍 Checking OpenAI Assistants API package versions..." +echo "" + +# Check if npm is installed +if ! command -v npm &> /dev/null; then + echo "❌ npm not found. Please install Node.js and npm." + exit 1 +fi + +# Check openai package +echo "📦 Checking openai package..." +INSTALLED_VERSION=$(npm list openai --depth=0 2>/dev/null | grep openai@ | sed 's/.*openai@//' | sed 's/ .*//' || echo "not installed") +REQUIRED_VERSION="6.7.0" + +if [ "$INSTALLED_VERSION" = "not installed" ]; then + echo " ❌ openai package not installed" + echo " Run: npm install openai@${REQUIRED_VERSION}" + exit 1 +fi + +# Compare versions +INSTALLED_MAJOR=$(echo $INSTALLED_VERSION | cut -d. -f1) +REQUIRED_MAJOR=$(echo $REQUIRED_VERSION | cut -d. -f1) + +if [ "$INSTALLED_MAJOR" -lt "$REQUIRED_MAJOR" ]; then + echo " ⚠️ Installed: $INSTALLED_VERSION (outdated)" + echo " ✅ Required: ${REQUIRED_VERSION}+" + echo " Run: npm install openai@latest" + exit 1 +else + echo " ✅ Installed: $INSTALLED_VERSION" +fi + +echo "" +echo "📋 Current package versions:" +npm list openai tsx typescript @types/node --depth=0 2>/dev/null || true + +echo "" +echo "✅ All package versions are compatible!" +echo "" +echo "💡 To update to latest versions:" +echo " npm install openai@latest tsx@latest typescript@latest @types/node@latest" diff --git a/templates/basic-assistant.ts b/templates/basic-assistant.ts new file mode 100644 index 0000000..e2adbc4 --- /dev/null +++ b/templates/basic-assistant.ts @@ -0,0 +1,87 @@ +/** + * Basic Assistant Example + * + * Demonstrates the fundamental workflow: + * 1. Create an assistant + * 2. Create a thread + * 3. Add a message + * 4. Create a run + * 5. Poll for completion + * 6. Retrieve the response + */ + +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +async function main() { + console.log('🤖 Creating Math Tutor Assistant...\n'); + + // 1. Create an assistant + const assistant = await openai.beta.assistants.create({ + name: "Math Tutor", + instructions: "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", + tools: [{ type: "code_interpreter" }], + model: "gpt-4o", + }); + + console.log(`✅ Assistant created: ${assistant.id}\n`); + + // 2. Create a thread + const thread = await openai.beta.threads.create(); + console.log(`✅ Thread created: ${thread.id}\n`); + + // 3. Add a message to the thread + await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "I need to solve the equation `3x + 11 = 14`. Can you help me?", + }); + + console.log('✅ Message added to thread\n'); + + // 4. Create a run + console.log('🏃 Creating run...\n'); + const run = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, + }); + + // 5. Poll for completion + console.log('⏳ Waiting for completion...\n'); + let runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id); + + while (runStatus.status !== 'completed') { + if (runStatus.status === 'failed' || runStatus.status === 'cancelled') { + console.error(`❌ Run ${runStatus.status}:`, runStatus.last_error); + process.exit(1); + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id); + console.log(` Status: ${runStatus.status}`); + } + + console.log('\n✅ Run completed!\n'); + + // 6. Retrieve messages + const messages = await openai.beta.threads.messages.list(thread.id); + + console.log('💬 Response:\n'); + const response = messages.data[0].content[0]; + if (response.type === 'text') { + console.log(response.text.value); + } + + // Usage stats + console.log('\n📊 Usage:'); + console.log(` Prompt tokens: ${runStatus.usage?.prompt_tokens}`); + console.log(` Completion tokens: ${runStatus.usage?.completion_tokens}`); + console.log(` Total tokens: ${runStatus.usage?.total_tokens}`); + + // Cleanup (optional) + // await openai.beta.assistants.del(assistant.id); + // await openai.beta.threads.del(thread.id); +} + +main().catch(console.error); diff --git a/templates/code-interpreter-assistant.ts b/templates/code-interpreter-assistant.ts new file mode 100644 index 0000000..395e460 --- /dev/null +++ b/templates/code-interpreter-assistant.ts @@ -0,0 +1,136 @@ +/** + * Code Interpreter Assistant + * + * Demonstrates: + * - Python code execution + * - File uploads for data analysis + * - Retrieving generated files (charts, CSVs) + * - Data visualization + */ + +import OpenAI from 'openai'; +import fs from 'fs'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +async function main() { + console.log('📊 Creating Data Analyst Assistant...\n'); + + // 1. Create assistant with code interpreter + const assistant = await openai.beta.assistants.create({ + name: "Data Analyst", + instructions: "You are a data analyst. Analyze data and create visualizations. Always explain your approach and findings.", + tools: [{ type: "code_interpreter" }], + model: "gpt-4o", + }); + + console.log(`✅ Assistant created: ${assistant.id}\n`); + + // 2. Upload a data file (CSV example) + // For this example, create a sample CSV + const csvData = `date,revenue,expenses +2025-01-01,10000,4000 +2025-01-02,12000,4500 +2025-01-03,9500,4200 +2025-01-04,15000,5000 +2025-01-05,13500,4800`; + + fs.writeFileSync('sample_data.csv', csvData); + + const file = await openai.files.create({ + file: fs.createReadStream('sample_data.csv'), + purpose: 'assistants', + }); + + console.log(`✅ File uploaded: ${file.id}\n`); + + // 3. Create thread with file attachment + const thread = await openai.beta.threads.create({ + messages: [{ + role: "user", + content: "Analyze this revenue data. Calculate total revenue, average daily revenue, and create a visualization showing revenue and expenses over time.", + attachments: [{ + file_id: file.id, + tools: [{ type: "code_interpreter" }], + }], + }], + }); + + console.log(`✅ Thread created: ${thread.id}\n`); + + // 4. Run the assistant + console.log('🏃 Running analysis...\n'); + const run = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, + }); + + // 5. Poll for completion + let runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id); + + while (!['completed', 'failed', 'cancelled'].includes(runStatus.status)) { + await new Promise(resolve => setTimeout(resolve, 1000)); + runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id); + console.log(` Status: ${runStatus.status}`); + } + + if (runStatus.status !== 'completed') { + console.error(`❌ Run ${runStatus.status}:`, runStatus.last_error); + process.exit(1); + } + + console.log('\n✅ Analysis completed!\n'); + + // 6. Retrieve the response + const messages = await openai.beta.threads.messages.list(thread.id); + const responseMessage = messages.data[0]; + + console.log('💬 Analysis Results:\n'); + + for (const content of responseMessage.content) { + if (content.type === 'text') { + console.log(content.text.value); + console.log('\n---\n'); + } + + // Download generated image files (charts) + if (content.type === 'image_file') { + const imageFileId = content.image_file.file_id; + console.log(`📈 Chart generated: ${imageFileId}`); + + // Download the image + const imageData = await openai.files.content(imageFileId); + const imageBuffer = Buffer.from(await imageData.arrayBuffer()); + + fs.writeFileSync(`chart_${imageFileId}.png`, imageBuffer); + console.log(` Saved as: chart_${imageFileId}.png\n`); + } + } + + // 7. Check run steps to see code that was executed + const runSteps = await openai.beta.threads.runs.steps.list(thread.id, run.id); + + console.log('🔍 Execution Steps:\n'); + for (const step of runSteps.data) { + if (step.step_details.type === 'tool_calls') { + for (const toolCall of step.step_details.tool_calls) { + if (toolCall.type === 'code_interpreter') { + console.log('Python code executed:'); + console.log(toolCall.code_interpreter.input); + console.log('\nOutput:'); + console.log(toolCall.code_interpreter.outputs); + console.log('\n---\n'); + } + } + } + } + + // Cleanup + fs.unlinkSync('sample_data.csv'); + + console.log('\n📊 Usage:'); + console.log(` Total tokens: ${runStatus.usage?.total_tokens}`); +} + +main().catch(console.error); diff --git a/templates/file-search-assistant.ts b/templates/file-search-assistant.ts new file mode 100644 index 0000000..616cce5 --- /dev/null +++ b/templates/file-search-assistant.ts @@ -0,0 +1,213 @@ +/** + * File Search Assistant (RAG) + * + * Demonstrates: + * - Creating a vector store + * - Uploading documents + * - Semantic search with file_search tool + * - Retrieving answers with citations + */ + +import OpenAI from 'openai'; +import fs from 'fs'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +async function main() { + console.log('📚 Creating Knowledge Base Assistant...\n'); + + // 1. Create a vector store + const vectorStore = await openai.beta.vectorStores.create({ + name: "Product Documentation", + expires_after: { + anchor: "last_active_at", + days: 7, // Auto-delete after 7 days of inactivity + }, + }); + + console.log(`✅ Vector store created: ${vectorStore.id}\n`); + + // 2. Upload documents to vector store + // Create sample documents + const doc1 = `# Product Installation Guide + +To install our product: +1. Download the installer from our website +2. Run the installer with administrator privileges +3. Follow the on-screen instructions +4. Restart your computer after installation + +System Requirements: +- Windows 10 or later / macOS 11 or later +- 4GB RAM minimum, 8GB recommended +- 500MB free disk space`; + + const doc2 = `# Troubleshooting Guide + +Common Issues: + +1. Installation Fails + - Ensure you have administrator privileges + - Disable antivirus temporarily + - Check disk space + +2. Application Won't Start + - Update graphics drivers + - Run compatibility troubleshooter + - Reinstall the application + +3. Performance Issues + - Close other applications + - Increase virtual memory + - Check for updates`; + + fs.writeFileSync('install_guide.md', doc1); + fs.writeFileSync('troubleshooting.md', doc2); + + // Upload files + const file1 = await openai.files.create({ + file: fs.createReadStream('install_guide.md'), + purpose: 'assistants', + }); + + const file2 = await openai.files.create({ + file: fs.createReadStream('troubleshooting.md'), + purpose: 'assistants', + }); + + console.log(`✅ Files uploaded: ${file1.id}, ${file2.id}\n`); + + // Add files to vector store (batch upload) + const fileBatch = await openai.beta.vectorStores.fileBatches.create( + vectorStore.id, + { + file_ids: [file1.id, file2.id], + } + ); + + console.log('⏳ Indexing files...\n'); + + // Poll for vector store completion + let batch = await openai.beta.vectorStores.fileBatches.retrieve( + vectorStore.id, + fileBatch.id + ); + + while (batch.status === 'in_progress') { + await new Promise(resolve => setTimeout(resolve, 1000)); + batch = await openai.beta.vectorStores.fileBatches.retrieve( + vectorStore.id, + fileBatch.id + ); + } + + console.log(`✅ Indexing complete! Status: ${batch.status}\n`); + + // 3. Create assistant with file search + const assistant = await openai.beta.assistants.create({ + name: "Product Support Assistant", + instructions: "You are a helpful product support assistant. Use the file search tool to answer questions about installation, troubleshooting, and product usage. Always cite your sources.", + tools: [{ type: "file_search" }], + tool_resources: { + file_search: { + vector_store_ids: [vectorStore.id], + }, + }, + model: "gpt-4o", + }); + + console.log(`✅ Assistant created: ${assistant.id}\n`); + + // 4. Create thread and ask questions + const thread = await openai.beta.threads.create(); + + await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "How do I install the product?", + }); + + console.log('❓ Question: How do I install the product?\n'); + + // 5. Run + const run = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, + }); + + // Poll for completion + let runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id); + + while (!['completed', 'failed'].includes(runStatus.status)) { + await new Promise(resolve => setTimeout(resolve, 1000)); + runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id); + } + + // 6. Retrieve response + const messages = await openai.beta.threads.messages.list(thread.id); + const response = messages.data[0]; + + console.log('💬 Answer:\n'); + for (const content of response.content) { + if (content.type === 'text') { + console.log(content.text.value); + + // Check for citations + if (content.text.annotations && content.text.annotations.length > 0) { + console.log('\n📎 Citations:'); + for (const annotation of content.text.annotations) { + if (annotation.type === 'file_citation') { + console.log(` File: ${annotation.file_citation.file_id}`); + console.log(` Quote: ${annotation.file_citation.quote}`); + } + } + } + } + } + + console.log('\n---\n'); + + // Ask another question + await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "What should I do if the application won't start?", + }); + + console.log('❓ Question: What should I do if the application won\'t start?\n'); + + const run2 = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, + }); + + let runStatus2 = await openai.beta.threads.runs.retrieve(thread.id, run2.id); + + while (!['completed', 'failed'].includes(runStatus2.status)) { + await new Promise(resolve => setTimeout(resolve, 1000)); + runStatus2 = await openai.beta.threads.runs.retrieve(thread.id, run2.id); + } + + const messages2 = await openai.beta.threads.messages.list(thread.id); + const response2 = messages2.data[0]; + + console.log('💬 Answer:\n'); + for (const content of response2.content) { + if (content.type === 'text') { + console.log(content.text.value); + } + } + + // 7. Vector store stats + const storeInfo = await openai.beta.vectorStores.retrieve(vectorStore.id); + console.log('\n📊 Vector Store Stats:'); + console.log(` Files: ${storeInfo.file_counts.completed}`); + console.log(` Size: ${storeInfo.usage_bytes} bytes`); + + // Cleanup + fs.unlinkSync('install_guide.md'); + fs.unlinkSync('troubleshooting.md'); + + console.log('\n💡 Note: Vector store will auto-delete after 7 days of inactivity'); + console.log(` Or manually delete with: await openai.beta.vectorStores.del("${vectorStore.id}")`); +} + +main().catch(console.error); diff --git a/templates/function-calling-assistant.ts b/templates/function-calling-assistant.ts new file mode 100644 index 0000000..3dd4f0a --- /dev/null +++ b/templates/function-calling-assistant.ts @@ -0,0 +1,247 @@ +/** + * Function Calling Assistant + * + * Demonstrates: + * - Defining custom functions + * - Handling requires_action state + * - Submitting tool outputs + * - Multiple function calls + */ + +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +// Mock weather API +async function getWeather(location: string, unit: 'celsius' | 'fahrenheit' = 'celsius') { + // In production, call a real weather API + const temps = { celsius: 22, fahrenheit: 72 }; + return { + location, + temperature: temps[unit], + unit, + conditions: "Partly cloudy", + humidity: 65, + }; +} + +// Mock stock price API +async function getStockPrice(symbol: string) { + // In production, call a real stock API + const prices: Record = { + AAPL: 185.50, + GOOGL: 142.75, + MSFT: 420.30, + }; + + return { + symbol, + price: prices[symbol] || 100.00, + currency: 'USD', + timestamp: new Date().toISOString(), + }; +} + +async function main() { + console.log('🛠️ Creating Function Calling Assistant...\n'); + + // 1. Create assistant with functions + const assistant = await openai.beta.assistants.create({ + name: "Multi-Tool Assistant", + instructions: "You are a helpful assistant that can check weather and stock prices. Use the available functions to answer user questions.", + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather for a location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city name, e.g., 'San Francisco'", + }, + unit: { + type: "string", + enum: ["celsius", "fahrenheit"], + description: "Temperature unit", + }, + }, + required: ["location"], + }, + }, + }, + { + type: "function", + function: { + name: "get_stock_price", + description: "Get the current stock price for a symbol", + parameters: { + type: "object", + properties: { + symbol: { + type: "string", + description: "Stock symbol, e.g., 'AAPL' for Apple", + }, + }, + required: ["symbol"], + }, + }, + }, + ], + model: "gpt-4o", + }); + + console.log(`✅ Assistant created: ${assistant.id}\n`); + + // 2. Create thread + const thread = await openai.beta.threads.create({ + messages: [{ + role: "user", + content: "What's the weather in London and what's Apple's stock price?", + }], + }); + + console.log(`✅ Thread created: ${thread.id}\n`); + console.log('❓ User: What\'s the weather in London and what\'s Apple\'s stock price?\n'); + + // 3. Create run + let run = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, + }); + + console.log('🏃 Running assistant...\n'); + + // 4. Poll and handle function calls + while (true) { + run = await openai.beta.threads.runs.retrieve(thread.id, run.id); + + console.log(` Status: ${run.status}`); + + if (run.status === 'completed') { + break; + } + + if (run.status === 'failed' || run.status === 'cancelled' || run.status === 'expired') { + console.error(`❌ Run ${run.status}:`, run.last_error); + process.exit(1); + } + + if (run.status === 'requires_action') { + console.log('\n🔧 Function calls required:\n'); + + const toolCalls = run.required_action!.submit_tool_outputs.tool_calls; + const toolOutputs = []; + + for (const toolCall of toolCalls) { + console.log(` Function: ${toolCall.function.name}`); + console.log(` Arguments: ${toolCall.function.arguments}`); + + const args = JSON.parse(toolCall.function.arguments); + let output; + + // Execute the function + if (toolCall.function.name === 'get_weather') { + output = await getWeather(args.location, args.unit); + console.log(` Result: ${output.temperature}°${output.unit === 'celsius' ? 'C' : 'F'}, ${output.conditions}`); + } else if (toolCall.function.name === 'get_stock_price') { + output = await getStockPrice(args.symbol); + console.log(` Result: $${output.price}`); + } + + toolOutputs.push({ + tool_call_id: toolCall.id, + output: JSON.stringify(output), + }); + + console.log(''); + } + + // Submit tool outputs + console.log('📤 Submitting tool outputs...\n'); + run = await openai.beta.threads.runs.submitToolOutputs( + thread.id, + run.id, + { tool_outputs: toolOutputs } + ); + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + console.log('\n✅ Run completed!\n'); + + // 5. Retrieve final response + const messages = await openai.beta.threads.messages.list(thread.id); + const response = messages.data[0]; + + console.log('💬 Assistant Response:\n'); + for (const content of response.content) { + if (content.type === 'text') { + console.log(content.text.value); + } + } + + // Ask another question + console.log('\n---\n'); + await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "How about Microsoft's stock?", + }); + + console.log('❓ User: How about Microsoft\'s stock?\n'); + + run = await openai.beta.threads.runs.create(thread.id, { + assistant_id: assistant.id, + }); + + // Handle function calls again + while (true) { + run = await openai.beta.threads.runs.retrieve(thread.id, run.id); + + if (run.status === 'completed') { + break; + } + + if (run.status === 'requires_action') { + const toolCalls = run.required_action!.submit_tool_outputs.tool_calls; + const toolOutputs = []; + + for (const toolCall of toolCalls) { + const args = JSON.parse(toolCall.function.arguments); + + if (toolCall.function.name === 'get_stock_price') { + const output = await getStockPrice(args.symbol); + console.log(` 🔧 Called get_stock_price(${args.symbol}): $${output.price}`); + toolOutputs.push({ + tool_call_id: toolCall.id, + output: JSON.stringify(output), + }); + } + } + + run = await openai.beta.threads.runs.submitToolOutputs( + thread.id, + run.id, + { tool_outputs: toolOutputs } + ); + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + const messages2 = await openai.beta.threads.messages.list(thread.id); + const response2 = messages2.data[0]; + + console.log('\n💬 Assistant Response:\n'); + for (const content of response2.content) { + if (content.type === 'text') { + console.log(content.text.value); + } + } +} + +main().catch(console.error); diff --git a/templates/package.json b/templates/package.json new file mode 100644 index 0000000..9e7531e --- /dev/null +++ b/templates/package.json @@ -0,0 +1,23 @@ +{ + "name": "openai-assistants-templates", + "version": "1.0.0", + "description": "OpenAI Assistants API v2 templates", + "type": "module", + "scripts": { + "basic": "tsx templates/basic-assistant.ts", + "code-interpreter": "tsx templates/code-interpreter-assistant.ts", + "file-search": "tsx templates/file-search-assistant.ts", + "function-calling": "tsx templates/function-calling-assistant.ts", + "streaming": "tsx templates/streaming-assistant.ts", + "thread-management": "tsx templates/thread-management.ts", + "vector-store": "tsx templates/vector-store-setup.ts" + }, + "dependencies": { + "openai": "^6.7.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "tsx": "^4.7.0", + "typescript": "^5.3.3" + } +} diff --git a/templates/streaming-assistant.ts b/templates/streaming-assistant.ts new file mode 100644 index 0000000..e3c8fa9 --- /dev/null +++ b/templates/streaming-assistant.ts @@ -0,0 +1,165 @@ +/** + * Streaming Assistant + * + * Demonstrates: + * - Real-time streaming with Server-Sent Events (SSE) + * - Handling different event types + * - Streaming message deltas + * - Tool call progress updates + */ + +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +async function main() { + console.log('🌊 Creating Streaming Assistant...\n'); + + // 1. Create assistant + const assistant = await openai.beta.assistants.create({ + name: "Streaming Tutor", + instructions: "You are a helpful tutor. Explain concepts clearly and use code interpreter when helpful.", + tools: [{ type: "code_interpreter" }], + model: "gpt-4o", + }); + + console.log(`✅ Assistant created: ${assistant.id}\n`); + + // 2. Create thread + const thread = await openai.beta.threads.create({ + messages: [{ + role: "user", + content: "Explain the Fibonacci sequence and calculate the first 10 numbers.", + }], + }); + + console.log(`✅ Thread created: ${thread.id}\n`); + console.log('💬 User: Explain the Fibonacci sequence and calculate the first 10 numbers.\n'); + console.log('🤖 Assistant: '); + + // 3. Create streaming run + const stream = await openai.beta.threads.runs.stream(thread.id, { + assistant_id: assistant.id, + }); + + // Track state + let currentMessageId: string | null = null; + let fullResponse = ''; + + // 4. Handle stream events + for await (const event of stream) { + switch (event.event) { + case 'thread.run.created': + console.log('[Run started]\n'); + break; + + case 'thread.run.queued': + console.log('[Run queued...]\n'); + break; + + case 'thread.run.in_progress': + console.log('[Processing...]\n'); + break; + + case 'thread.message.created': + currentMessageId = event.data.id; + break; + + case 'thread.message.delta': + // Stream text content in real-time + const delta = event.data.delta.content?.[0]; + if (delta?.type === 'text' && delta.text?.value) { + process.stdout.write(delta.text.value); + fullResponse += delta.text.value; + } + break; + + case 'thread.message.completed': + console.log('\n\n[Message completed]\n'); + break; + + case 'thread.run.step.created': + const step = event.data; + if (step.type === 'tool_calls') { + console.log('\n[Tool call initiated...]\n'); + } + break; + + case 'thread.run.step.delta': + // Show code interpreter progress + const stepDelta = event.data.delta.step_details; + if (stepDelta?.type === 'tool_calls') { + const toolCall = stepDelta.tool_calls?.[0]; + + if (toolCall?.type === 'code_interpreter') { + if (toolCall.code_interpreter?.input) { + console.log('\n🔧 Executing Python code:\n'); + console.log(toolCall.code_interpreter.input); + console.log('\n'); + } + + if (toolCall.code_interpreter?.outputs) { + for (const output of toolCall.code_interpreter.outputs) { + if (output.type === 'logs') { + console.log('📋 Output:', output.logs); + } + } + } + } + } + break; + + case 'thread.run.step.completed': + console.log('[Step completed]\n'); + break; + + case 'thread.run.completed': + console.log('\n✅ Run completed!\n'); + break; + + case 'thread.run.failed': + console.error('\n❌ Run failed:', event.data.last_error); + break; + + case 'thread.run.requires_action': + console.log('\n⚠️ Requires action (function calling needed)'); + break; + + case 'error': + console.error('\n❌ Stream error:', event.data); + break; + } + } + + console.log('---\n'); + + // Ask a follow-up question + await openai.beta.threads.messages.create(thread.id, { + role: "user", + content: "Can you explain how recursion works in that sequence?", + }); + + console.log('💬 User: Can you explain how recursion works in that sequence?\n'); + console.log('🤖 Assistant: '); + + const stream2 = await openai.beta.threads.runs.stream(thread.id, { + assistant_id: assistant.id, + }); + + for await (const event of stream2) { + if (event.event === 'thread.message.delta') { + const delta = event.data.delta.content?.[0]; + if (delta?.type === 'text' && delta.text?.value) { + process.stdout.write(delta.text.value); + } + } + + if (event.event === 'thread.run.completed') { + console.log('\n\n✅ Streaming complete!\n'); + } + } +} + +main().catch(console.error); diff --git a/templates/thread-management.ts b/templates/thread-management.ts new file mode 100644 index 0000000..66cb91b --- /dev/null +++ b/templates/thread-management.ts @@ -0,0 +1,237 @@ +/** + * Thread Lifecycle Management + * + * Demonstrates: + * - Creating and reusing threads + * - Checking for active runs + * - Thread cleanup patterns + * - Error handling for common issues + */ + +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +// Simulate database storage +const userThreads = new Map(); + +/** + * Get or create a thread for a user + */ +async function getOrCreateUserThread(userId: string): Promise { + console.log(`🔍 Checking thread for user: ${userId}`); + + // Check if thread exists + let threadId = userThreads.get(userId); + + if (!threadId) { + console.log(' No existing thread found. Creating new thread...'); + const thread = await openai.beta.threads.create({ + metadata: { + user_id: userId, + created_at: new Date().toISOString(), + }, + }); + threadId = thread.id; + userThreads.set(userId, threadId); + console.log(` ✅ Thread created: ${threadId}`); + } else { + console.log(` ✅ Existing thread found: ${threadId}`); + } + + return threadId; +} + +/** + * Check if thread has an active run + */ +async function hasActiveRun(threadId: string): Promise { + const runs = await openai.beta.threads.runs.list(threadId, { + limit: 1, + order: 'desc', + }); + + const latestRun = runs.data[0]; + return latestRun && ['queued', 'in_progress', 'cancelling'].includes(latestRun.status); +} + +/** + * Wait for active run to complete or cancel it + */ +async function ensureNoActiveRun(threadId: string, cancelIfActive = false) { + const runs = await openai.beta.threads.runs.list(threadId, { + limit: 1, + order: 'desc', + }); + + const latestRun = runs.data[0]; + + if (latestRun && ['queued', 'in_progress', 'cancelling'].includes(latestRun.status)) { + if (cancelIfActive) { + console.log(` ⚠️ Active run detected: ${latestRun.id}. Cancelling...`); + await openai.beta.threads.runs.cancel(threadId, latestRun.id); + + // Wait for cancellation + let run = latestRun; + while (run.status !== 'cancelled') { + await new Promise(resolve => setTimeout(resolve, 500)); + run = await openai.beta.threads.runs.retrieve(threadId, run.id); + } + console.log(' ✅ Run cancelled'); + } else { + throw new Error( + `Thread ${threadId} has an active run (${latestRun.id}). ` + + `Wait for completion or set cancelIfActive=true` + ); + } + } +} + +/** + * Send a message safely (check for active runs first) + */ +async function sendMessage( + threadId: string, + assistantId: string, + message: string +): Promise { + console.log(`\n💬 Sending message to thread ${threadId}...`); + + // Ensure no active run + await ensureNoActiveRun(threadId, true); + + // Add message + await openai.beta.threads.messages.create(threadId, { + role: 'user', + content: message, + }); + + console.log(' ✅ Message added'); + + // Create run + console.log(' 🏃 Creating run...'); + const run = await openai.beta.threads.runs.create(threadId, { + assistant_id: assistantId, + }); + + // Poll for completion + let runStatus = await openai.beta.threads.runs.retrieve(threadId, run.id); + + while (!['completed', 'failed', 'cancelled'].includes(runStatus.status)) { + await new Promise(resolve => setTimeout(resolve, 1000)); + runStatus = await openai.beta.threads.runs.retrieve(threadId, run.id); + } + + if (runStatus.status !== 'completed') { + throw new Error(`Run ${runStatus.status}: ${runStatus.last_error?.message}`); + } + + console.log(' ✅ Run completed'); + + // Get response + const messages = await openai.beta.threads.messages.list(threadId, { + limit: 1, + order: 'desc', + }); + + const responseContent = messages.data[0].content[0]; + if (responseContent.type === 'text') { + return responseContent.text.value; + } + + return ''; +} + +/** + * Clean up old threads + */ +async function cleanupOldThreads(maxAgeHours: number = 24) { + console.log(`\n🧹 Cleaning up threads older than ${maxAgeHours} hours...`); + + let deletedCount = 0; + + for (const [userId, threadId] of userThreads.entries()) { + try { + const thread = await openai.beta.threads.retrieve(threadId); + const createdAt = new Date(thread.created_at * 1000); + const ageHours = (Date.now() - createdAt.getTime()) / (1000 * 60 * 60); + + if (ageHours > maxAgeHours) { + await openai.beta.threads.del(threadId); + userThreads.delete(userId); + deletedCount++; + console.log(` ✅ Deleted thread for user ${userId} (age: ${ageHours.toFixed(1)}h)`); + } + } catch (error) { + console.error(` ❌ Error deleting thread ${threadId}:`, error); + } + } + + console.log(`\n Total threads deleted: ${deletedCount}`); +} + +/** + * Main demo + */ +async function main() { + console.log('🧵 Thread Lifecycle Management Demo\n'); + + // Create an assistant + const assistant = await openai.beta.assistants.create({ + name: "Demo Assistant", + instructions: "You are a helpful assistant.", + model: "gpt-4o", + }); + + console.log(`✅ Assistant created: ${assistant.id}\n`); + + // Simulate multiple users + const user1 = 'user_alice'; + const user2 = 'user_bob'; + + // User 1: First message + let thread1 = await getOrCreateUserThread(user1); + let response1 = await sendMessage(thread1, assistant.id, "Hello! What's 2+2?"); + console.log(`\n🤖 Response: ${response1}\n`); + + // User 2: First message + let thread2 = await getOrCreateUserThread(user2); + let response2 = await sendMessage(thread2, assistant.id, "What's the capital of France?"); + console.log(`\n🤖 Response: ${response2}\n`); + + // User 1: Second message (reuses thread) + thread1 = await getOrCreateUserThread(user1); + response1 = await sendMessage(thread1, assistant.id, "Can you multiply that by 3?"); + console.log(`\n🤖 Response: ${response1}\n`); + + // Check for active runs + console.log('\n📊 Thread Status:'); + const hasActive1 = await hasActiveRun(thread1); + const hasActive2 = await hasActiveRun(thread2); + console.log(` User 1 thread active: ${hasActive1}`); + console.log(` User 2 thread active: ${hasActive2}`); + + // List messages in thread 1 + console.log(`\n📜 Conversation history for user 1:`); + const messages = await openai.beta.threads.messages.list(thread1); + for (const message of messages.data.reverse()) { + const content = message.content[0]; + if (content.type === 'text') { + console.log(` ${message.role}: ${content.text.value}`); + } + } + + // Cleanup demo (set to 0 hours to delete all) + // await cleanupOldThreads(0); + + console.log('\n✅ Demo complete!'); + console.log(`\n💡 Tips:`); + console.log(' - Always check for active runs before creating new ones'); + console.log(' - Reuse threads for conversation continuity'); + console.log(' - Clean up old threads to manage costs'); + console.log(' - Use metadata to track thread ownership'); +} + +main().catch(console.error); diff --git a/templates/vector-store-setup.ts b/templates/vector-store-setup.ts new file mode 100644 index 0000000..7720bdd --- /dev/null +++ b/templates/vector-store-setup.ts @@ -0,0 +1,241 @@ +/** + * Vector Store Setup + * + * Demonstrates: + * - Creating vector stores + * - Batch file uploads + * - Monitoring indexing progress + * - Vector store management + * - Cost optimization + */ + +import OpenAI from 'openai'; +import fs from 'fs'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +async function main() { + console.log('📦 Vector Store Setup Guide\n'); + + // 1. Create a vector store + console.log('Step 1: Creating vector store...\n'); + + const vectorStore = await openai.beta.vectorStores.create({ + name: "Company Knowledge Base", + metadata: { + department: "engineering", + version: "1.0", + }, + expires_after: { + anchor: "last_active_at", + days: 30, // Auto-delete after 30 days of inactivity + }, + }); + + console.log(`✅ Vector store created: ${vectorStore.id}`); + console.log(` Name: ${vectorStore.name}`); + console.log(` Status: ${vectorStore.status}`); + console.log(` Auto-expires: ${vectorStore.expires_after?.days} days after last use\n`); + + // 2. Prepare sample documents + console.log('Step 2: Preparing documents...\n'); + + const documents = [ + { + filename: 'api_docs.md', + content: `# API Documentation + +## Authentication +All API requests require an API key in the Authorization header. + +## Rate Limits +- Free tier: 100 requests/hour +- Pro tier: 1000 requests/hour + +## Endpoints +- GET /api/users - List users +- POST /api/users - Create user +- GET /api/users/:id - Get user details`, + }, + { + filename: 'deployment_guide.md', + content: `# Deployment Guide + +## Prerequisites +- Docker installed +- Kubernetes cluster running +- kubectl configured + +## Steps +1. Build Docker image: docker build -t app:latest . +2. Push to registry: docker push registry/app:latest +3. Deploy: kubectl apply -f deployment.yaml +4. Verify: kubectl get pods`, + }, + { + filename: 'security_policy.md', + content: `# Security Policy + +## Password Requirements +- Minimum 12 characters +- Must include uppercase, lowercase, numbers, symbols +- Cannot reuse last 5 passwords + +## Access Control +- Use SSO for authentication +- Enable 2FA for all accounts +- Review access logs monthly + +## Incident Response +- Report security issues to security@company.com +- Critical incidents escalated within 1 hour`, + }, + ]; + + // Write files to disk + const fileIds: string[] = []; + + for (const doc of documents) { + fs.writeFileSync(doc.filename, doc.content); + console.log(` 📄 Created: ${doc.filename}`); + } + + // 3. Upload files + console.log('\nStep 3: Uploading files to OpenAI...\n'); + + for (const doc of documents) { + const file = await openai.files.create({ + file: fs.createReadStream(doc.filename), + purpose: 'assistants', + }); + + fileIds.push(file.id); + console.log(` ✅ Uploaded: ${doc.filename} (${file.id})`); + + // Clean up local file + fs.unlinkSync(doc.filename); + } + + // 4. Add files to vector store (batch upload) + console.log('\nStep 4: Adding files to vector store...\n'); + + const fileBatch = await openai.beta.vectorStores.fileBatches.create( + vectorStore.id, + { + file_ids: fileIds, + } + ); + + console.log(` 📦 Batch created: ${fileBatch.id}`); + console.log(` Files in batch: ${fileBatch.file_counts.total}`); + + // 5. Monitor indexing progress + console.log('\nStep 5: Monitoring indexing progress...\n'); + + let batch = fileBatch; + let lastStatus = ''; + + while (batch.status === 'in_progress') { + batch = await openai.beta.vectorStores.fileBatches.retrieve( + vectorStore.id, + fileBatch.id + ); + + const statusMsg = ` Status: ${batch.status} | ` + + `Completed: ${batch.file_counts.completed}/${batch.file_counts.total} | ` + + `Failed: ${batch.file_counts.failed}`; + + if (statusMsg !== lastStatus) { + console.log(statusMsg); + lastStatus = statusMsg; + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + console.log(`\n ✅ Indexing ${batch.status}!`); + + if (batch.file_counts.failed > 0) { + console.log(` ⚠️ ${batch.file_counts.failed} files failed to index`); + + // List failed files + const files = await openai.beta.vectorStores.files.list(vectorStore.id); + for (const file of files.data) { + if (file.status === 'failed') { + console.log(` - File ${file.id}: ${file.last_error?.message}`); + } + } + } + + // 6. Get vector store details + console.log('\nStep 6: Vector store statistics...\n'); + + const updatedStore = await openai.beta.vectorStores.retrieve(vectorStore.id); + + console.log(` 📊 Statistics:`); + console.log(` Total files: ${updatedStore.file_counts.completed}`); + console.log(` Storage used: ${updatedStore.usage_bytes} bytes (${(updatedStore.usage_bytes / 1024 / 1024).toFixed(2)} MB)`); + console.log(` Status: ${updatedStore.status}`); + + // 7. Cost estimation + const storageMB = updatedStore.usage_bytes / 1024 / 1024; + const storageGB = storageMB / 1024; + const costPerDay = Math.max(0, (storageGB - 1) * 0.10); // First 1GB free, then $0.10/GB/day + const costPerMonth = costPerDay * 30; + + console.log(`\n 💰 Cost Estimation:`); + console.log(` Storage: ${storageGB.toFixed(4)} GB`); + console.log(` Cost per day: $${costPerDay.toFixed(4)} (first 1GB free)`); + console.log(` Cost per month: $${costPerMonth.toFixed(2)}`); + + // 8. List all files in vector store + console.log('\nStep 7: Files in vector store...\n'); + + const filesInStore = await openai.beta.vectorStores.files.list(vectorStore.id); + + for (const file of filesInStore.data) { + console.log(` 📄 ${file.id}`); + console.log(` Status: ${file.status}`); + console.log(` Created: ${new Date(file.created_at * 1000).toISOString()}`); + } + + // 9. Management operations + console.log('\nStep 8: Management operations...\n'); + + // Update vector store metadata + const updated = await openai.beta.vectorStores.update(vectorStore.id, { + metadata: { + department: "engineering", + version: "1.0", + last_updated: new Date().toISOString(), + }, + }); + + console.log(' ✅ Metadata updated'); + + // List all vector stores + const allStores = await openai.beta.vectorStores.list({ limit: 5 }); + console.log(`\n 📚 Total vector stores in account: ${allStores.data.length}`); + + for (const store of allStores.data) { + console.log(` - ${store.name} (${store.id}): ${store.file_counts.completed} files`); + } + + // 10. Cleanup instructions + console.log('\n💡 Cleanup Instructions:\n'); + console.log(' To delete individual files:'); + console.log(` await openai.beta.vectorStores.files.del("${vectorStore.id}", "file_id");`); + console.log(''); + console.log(' To delete entire vector store:'); + console.log(` await openai.beta.vectorStores.del("${vectorStore.id}");`); + console.log(''); + console.log(' Note: Vector store will auto-delete after 30 days of inactivity (configured above)'); + + console.log('\n✅ Vector store setup complete!'); + console.log(`\n🔑 Save this ID to use with assistants:`); + console.log(` ${vectorStore.id}`); +} + +main().catch(console.error);