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

FunctionDescription
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

FunctionDescription
createBedrockClient(config)Create Bedrock client
createBedrockRuntimeClient(config)Create runtime client
invokeModel(client, params)Invoke model
checkModelAccess(client, modelId)Check access

Text Utilities

FunctionDescription
ask(question, options)Generate text
summarize(options)Summarize text

| analyzeSentiment(text) | Analyze sentiment |