AI Package
On this page 37
An AI integration package providing unified access to multiple AI providers including Anthropic (Claude), OpenAI, Ollama, and AWS Bedrock.
Installation
bun add @stacksjs/ai
Basic Usage
import { anthropic, openai, ollama } from '@stacksjs/ai'
// Using Anthropic Claude
const response = await anthropic.chat({
messages: [{ role: 'user', content: 'Hello, Claude!' }]
})
// Using OpenAI
const gptResponse = await openai.chat({
messages: [{ role: 'user', content: 'Hello, GPT!' }]
})
// Using Ollama (local models)
const localResponse = await ollama.chat({
model: 'llama2',
messages: [{ role: 'user', content: 'Hello, Llama!' }]
})
Configuration
Configure AI providers in config/ai.ts:
export default {
// Default AI provider
default: 'anthropic',
// Anthropic configuration
anthropic: {
apiKey: process.env.ANTHROPIC_API_KEY,
model: 'claude-3-opus-20240229',
maxTokens: 4096,
},
// OpenAI configuration
openai: {
apiKey: process.env.OPENAI_API_KEY,
model: 'gpt-4-turbo-preview',
maxTokens: 4096,
organization: process.env.OPENAI_ORG_ID,
},
// Ollama configuration (local)
ollama: {
host: 'http://localhost:11434',
model: 'llama2',
},
// AWS Bedrock configuration
bedrock: {
region: process.env.AWS_DEFAULT_REGION || 'us-east-1',
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
model: 'anthropic.claude-3-sonnet-20240229-v1:0',
},
}
Anthropic (Claude)
Basic Chat
import { anthropic } from '@stacksjs/ai'
const response = await anthropic.chat({
model: 'claude-3-opus-20240229',
messages: [
{ role: 'user', content: 'Explain quantum computing in simple terms.' }
],
maxTokens: 1024,
})
console.log(response.content)
Streaming Responses
import { anthropic } from '@stacksjs/ai'
const stream = await anthropic.stream({
model: 'claude-3-sonnet-20240229',
messages: [
{ role: 'user', content: 'Write a short story about a robot.' }
],
})
for await (const chunk of stream) {
process.stdout.write(chunk.content || '')
}
System Prompts
import { anthropic } from '@stacksjs/ai'
const response = await anthropic.chat({
model: 'claude-3-opus-20240229',
system: 'You are a helpful coding assistant. Provide concise, accurate answers.',
messages: [
{ role: 'user', content: 'How do I sort an array in TypeScript?' }
],
})
Multi-turn Conversations
import { anthropic } from '@stacksjs/ai'
const conversation = [
{ role: 'user', content: 'What is the capital of France?' },
{ role: 'assistant', content: 'The capital of France is Paris.' },
{ role: 'user', content: 'What is its population?' }
]
const response = await anthropic.chat({
messages: conversation,
})
// Claude knows "its" refers to Paris from context
Vision (Image Analysis)
import { anthropic } from '@stacksjs/ai'
import { readFile } from 'node:fs/promises'
const imageData = await readFile('image.png')
const base64Image = imageData.toString('base64')
const response = await anthropic.chat({
model: 'claude-3-opus-20240229',
messages: [
{
role: 'user',
content: [
{
type: 'image',
source: {
type: 'base64',
media_type: 'image/png',
data: base64Image,
},
},
{
type: 'text',
text: 'What do you see in this image?',
},
],
},
],
})
OpenAI
Basic Chat
import { openai } from '@stacksjs/ai'
const response = await openai.chat({
model: 'gpt-4-turbo-preview',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'What is the meaning of life?' }
],
})
console.log(response.choices[0].message.content)
Streaming
import { openai } from '@stacksjs/ai'
const stream = await openai.stream({
model: 'gpt-4-turbo-preview',
messages: [
{ role: 'user', content: 'Tell me a joke.' }
],
})
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content
if (content) process.stdout.write(content)
}
Function Calling
import { openai } from '@stacksjs/ai'
const response = await openai.chat({
model: 'gpt-4-turbo-preview',
messages: [
{ role: 'user', content: 'What is the weather in San Francisco?' }
],
tools: [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get the current weather in a location',
parameters: {
type: 'object',
properties: {
location: {
type: 'string',
description: 'The city and state, e.g. San Francisco, CA',
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
},
},
required: ['location'],
},
},
},
],
})
// Handle function call
if (response.choices[0].message.tool_calls) {
const toolCall = response.choices[0].message.tool_calls[0]
const args = JSON.parse(toolCall.function.arguments)
// Call your weather API
const weather = await getWeather(args.location, args.unit)
// Continue conversation with function result
const followUp = await openai.chat({
model: 'gpt-4-turbo-preview',
messages: [
...messages,
response.choices[0].message,
{
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(weather),
},
],
})
}
Embeddings
import { openai } from '@stacksjs/ai'
const embedding = await openai.embed({
model: 'text-embedding-3-small',
input: 'The quick brown fox jumps over the lazy dog.',
})
console.log(embedding.data[0].embedding) // Vector of floats
Ollama (Local Models)
Basic Chat
import { ollama } from '@stacksjs/ai'
const response = await ollama.chat({
model: 'llama2',
messages: [
{ role: 'user', content: 'Hello! How are you?' }
],
})
console.log(response.message.content)
Available Models
import { ollama } from '@stacksjs/ai'
// List installed models
const models = await ollama.list()
console.log(models)
// Pull a new model
await ollama.pull('mistral')
// Use specific model
const response = await ollama.chat({
model: 'codellama',
messages: [
{ role: 'user', content: 'Write a Python function to reverse a string.' }
],
})
Streaming with Ollama
import { ollama } from '@stacksjs/ai'
const stream = await ollama.stream({
model: 'llama2',
messages: [
{ role: 'user', content: 'Explain machine learning.' }
],
})
for await (const chunk of stream) {
process.stdout.write(chunk.message?.content || '')
}
AWS Bedrock
Using Bedrock Client
There is no client to construct. The Bedrock clients are created lazily from
config/ai.ts and the AWS environment, so the functions take the call's
parameters directly:
import { invokeModel, listFoundationModels, requestModelAccess } from '@stacksjs/ai'
// Request access to every model named in config/ai.ts
await requestModelAccess()
// What the account can actually reach
const { modelSummaries } = await listFoundationModels({})
const response = await invokeModel({
modelId: 'anthropic.claude-3-sonnet-20240229-v1:0',
contentType: 'application/json',
body: JSON.stringify({
anthropic_version: 'bedrock-2023-05-31',
max_tokens: 1024,
messages: [
{ role: 'user', content: 'Hello!' },
],
}),
})
invokeModelWithResponseStream has the same shape and yields chunks.
AI Agents
Creating Agents
There is no createAgent with a tools array. Agents in Stacks are drivers
that delegate to the Claude CLI - locally or over SSH on an EC2 box - and the
tools are the CLI's own:
import { claudeAgent } from '@stacksjs/ai'
// Runs the `claude` CLI in a working directory
const local = claudeAgent.createLocal({ cwd: process.cwd() })
// Or over SSH, from BUDDY_EC2_HOST / BUDDY_EC2_USER / BUDDY_EC2_KEY
const remote = claudeAgent.createEC2({ ec2Host: 'agent.example.com' })
const answer = await local.process(
'Summarize the recent changes in this repository',
context,
history,
)
local.process(command, context, history) is the driver interface: the history
is passed in, so a driver holds no conversation state of its own.
For streaming, and for sessions that resume:
import { claudeAgentSDK, processCommandStreaming } from '@stacksjs/ai'
const result = await processCommandStreaming('Refactor this module', process.cwd())
const sessionId = claudeAgentSDK.getLastSessionId()
await claudeAgentSDK.resumeSession(sessionId, 'Now add tests')
claudeAgentSDK.clearSession()
Conversation memory
There is no MemoryStore class. Conversation state lives in buddyState,
which the Buddy command loop reads and writes:
import { buddyState } from '@stacksjs/ai'
buddyState.addToHistory({ role: 'user', content: 'My name is John' })
const { conversationHistory } = buddyState.getState()
buddyState.clearHistory()
Buddy - Voice AI Assistant
Using Buddy
Buddy is a repository-scoped command loop, not a voice assistant class - there
is no Buddy, no createBuddy, and no speech in the package. It opens a repo,
then processes commands against it:
import { buddyState, openRepository, processCommand } from '@stacksjs/ai'
// Clone or open a repository; the path or a GitHub URL both work.
const repo = await openRepository('https://github.com/stacksjs/stacks')
buddyState.setRepo(repo)
const answer = await processCommand('What does the queue package do?')
processCommand throws if no repository is open - it is the context every
command is answered against. Streaming has the same shape:
import { buddyProcessStreaming } from '@stacksjs/ai'
// A ReadableStream to consume now, plus the assembled text when it ends -
// so a caller can render as it arrives and still log the whole answer.
const { stream, fullResponse } = await buddyProcessStreaming('Explain the router')
for await (const chunk of stream)
process.stdout.write(chunk)
const complete = await fullResponse
Text Utilities
Text Generation
import { analyzeSentiment, ask, classifyText, summarize } from '@stacksjs/ai'
// Free-form generation. The prompt is the first argument, not an option.
const generated = await ask('Write a product description for a smart watch', {
maxTokenCount: 200,
})
const summary = await summarize(longArticle, { maxTokenCount: 100 })
// Sentiment and classification take the text first, then their own arguments.
const sentiment = await analyzeSentiment('This product exceeded my expectations')
const category = await classifyText(ticket, ['billing', 'bug', 'feature request'])
There is no translate. Ask for it:
const translated = await ask(`Translate to Spanish: ${'Hello, how are you?'}`)
Sentiment Analysis
import { analyzeSentiment } from '@stacksjs/ai'
const sentiment = await analyzeSentiment(
'I absolutely love this product! It exceeded all my expectations.'
)
// Returns: { sentiment: 'positive', score: 0.95 }
Error Handling
import { anthropic } from '@stacksjs/ai'
try {
const response = await anthropic.chat({
messages: [{ role: 'user', content: 'Hello' }],
})
} catch (error) {
if (error.status === 429) {
// Rate limited
console.log('Too many requests, waiting...')
await delay(error.headers['retry-after'] _ 1000)
} else if (error.status === 401) {
// Invalid API key
console.error('Invalid API key')
} else if (error.status === 500) {
// Server error
console.error('AI provider error')
}
}
Edge Cases
Handling Long Conversations
import { anthropic } from '@stacksjs/ai'
// Truncate or summarize old messages to stay within token limits
function trimConversation(messages: Message[], maxTokens: number) {
// Keep system message and recent messages
const systemMessage = messages.find(m => m.role === 'system')
const recentMessages = messages.slice(-10)
return systemMessage
? [systemMessage, ...recentMessages]
: recentMessages
}
const trimmedMessages = trimConversation(conversation, 4096)
const response = await anthropic.chat({ messages: trimmedMessages })
Retrying Failed Requests
import { anthropic } from '@stacksjs/ai'
async function chatWithRetry(
messages: Message[],
maxRetries = 3
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await anthropic.chat({ messages })
} catch (error) {
if (attempt === maxRetries) throw error
const delay = Math.pow(2, attempt) _ 1000 // Exponential backoff
await new Promise(r => setTimeout(r, delay))
}
}
}
Timeout Handling
import { anthropic } from '@stacksjs/ai'
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 30000) // 30 second timeout
try {
const response = await anthropic.chat({
messages: [{ role: 'user', content: 'Complex question...' }],
signal: controller.signal,
})
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request timed out')
}
} finally {
clearTimeout(timeoutId)
}
API Reference
Provider Functions
| Function | Description |
|---|---|
anthropic.chat(options) | Chat with Claude |
anthropic.stream(options) | Stream Claude response |
openai.chat(options) | Chat with GPT |
openai.stream(options) | Stream GPT response |
openai.embed(options) | Generate embeddings |
ollama.chat(options) | Chat with local model |
ollama.stream(options) | Stream local model |
ollama.list() | List installed models |
ollama.pull(model) | Download model |
Bedrock Functions
| Function | Description |
|---|---|
createBedrockClient(config) | Create Bedrock client |
createBedrockRuntimeClient(config) | Create runtime client |
invokeModel(client, params) | Invoke model |
checkModelAccess(client, modelId) | Check access |
Text Utilities
| Function | Description |
|---|---|
ask(question, options) | Generate text |
summarize(options) | Summarize text |
| analyzeSentiment(text) | Analyze sentiment |