DEV Community

Cover image for Building an AI-Powered Oracle Card Reading System with OpenAI Function Calling
Tarotap
Tarotap

Posted on

Building an AI-Powered Oracle Card Reading System with OpenAI Function Calling

Ever wondered how to combine the mystical world of oracle cards with cutting-edge AI? Today, I'm sharing how I built an intelligent Oracle Card reading system using OpenAI's function calling capabilities. This isn't just another chatbot - it's a full-fledged spiritual guidance agent that knows when to draw cards, interpret meanings, and even create personalized oracle cards with AI-generated artwork.

🔮 See It In Action

Check out the live implementation at Tarotap - Oracle Cards to experience AI-powered spiritual guidance firsthand.

🎯 What Makes This Special?

Unlike traditional random card generators, this system uses OpenAI's function calling to create an autonomous agent that:

  • Intelligently decides when to draw oracle cards based on context
  • Provides personalized interpretations considering the full conversation
  • Creates custom oracle cards with unique AI-generated imagery
  • Maintains conversation flow naturally between card readings

🏗️ The Architecture

Let's dive into the code. The core magic happens through OpenAI's function calling API, which allows the LLM to autonomously choose when and how to interact with our oracle card system.

Setting Up OpenAI with Function Calling

import OpenAI from 'openai';
import { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// Define our oracle card tools
const oracleTools: ChatCompletionTool[] = [
  {
    type: 'function',
    function: {
      name: 'drawOracleCard',
      description: 'Draw a random oracle card when the user asks for guidance or a reading',
      parameters: {
        type: 'object',
        properties: {
          question: {
            type: 'string',
            description: 'The user\'s question or area of focus for the reading'
          }
        },
        required: ['question']
      }
    }
  },
  {
    type: 'function',
    function: {
      name: 'createPersonalOracleCard',
      description: 'Create a personalized oracle card with AI-generated imagery',
      parameters: {
        type: 'object',
        properties: {
          cardName: {
            type: 'string',
            description: 'The name of the oracle card'
          },
          cardMeaning: {
            type: 'string',
            description: 'The spiritual meaning and guidance of the card'
          },
          artDescription: {
            type: 'string',
            description: 'Description for the AI image generation'
          }
        },
        required: ['cardName', 'cardMeaning', 'artDescription']
      }
    }
  }
];
Enter fullscreen mode Exit fullscreen mode

The Core Agent Loop

Here's where the magic happens - our agent decides autonomously when to use which tool:

async function processOracleReading(messages: ChatCompletionMessageParam[]) {
  try {
    const response = await openai.chat.completions.create({
      model: 'gpt-4-turbo-preview',
      messages: messages,
      tools: oracleTools,
      tool_choice: 'auto', // Let the model decide when to use tools
      temperature: 0.7,
      max_tokens: 1000,
    });

    const message = response.choices[0]?.message;

    if (!message) {
      throw new Error('No response from OpenAI');
    }

    // Handle tool calls
    if (message.tool_calls) {
      const toolResults = await Promise.all(
        message.tool_calls.map(async (toolCall) => {
          if (toolCall.function.name === 'drawOracleCard') {
            const args = JSON.parse(toolCall.function.arguments);
            const card = await drawRandomOracleCard(args.question);

            return {
              tool_call_id: toolCall.id,
              role: 'tool' as const,
              content: JSON.stringify(card)
            };
          }

          if (toolCall.function.name === 'createPersonalOracleCard') {
            const args = JSON.parse(toolCall.function.arguments);
            const imageUrl = await generateOracleCardImage(args);

            return {
              tool_call_id: toolCall.id,
              role: 'tool' as const,
              content: JSON.stringify({ 
                success: true, 
                imageUrl,
                cardName: args.cardName 
              })
            };
          }
        })
      );

      // Continue the conversation with tool results
      messages.push(message);
      messages.push(...toolResults);

      // Get final response
      return await processOracleReading(messages);
    }

    return message;
  } catch (error) {
    console.error('Oracle reading error:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Implementing the Oracle Card Drawing Logic

interface OracleCard {
  id: string;
  name: string;
  message: string;
  keywords: string[];
}

// Sample oracle cards data
const oracleCards: OracleCard[] = [
  {
    id: '1',
    name: 'New Beginnings',
    message: 'A fresh start awaits you. Trust in the journey ahead and embrace the unknown with open arms.',
    keywords: ['fresh start', 'opportunity', 'courage']
  },
  {
    id: '2',
    name: 'Inner Wisdom',
    message: 'Look within for the answers you seek. Your intuition is your greatest guide.',
    keywords: ['intuition', 'self-trust', 'meditation']
  },
  // ... more cards
];

async function drawRandomOracleCard(question: string): Promise<OracleCard> {
  // You could add more sophisticated logic here
  // For example, selecting cards based on keywords in the question
  const randomIndex = Math.floor(Math.random() * oracleCards.length);
  return oracleCards[randomIndex];
}
Enter fullscreen mode Exit fullscreen mode

Creating Custom Oracle Cards with DALL-E

One of the coolest features is the ability to create personalized oracle cards:

async function generateOracleCardImage(params: {
  cardName: string;
  cardMeaning: string;
  artDescription: string;
}): Promise<string> {
  try {
    const imageResponse = await openai.images.generate({
      model: 'dall-e-3',
      prompt: `Create a mystical oracle card illustration: ${params.artDescription}. 
               Style: Ethereal, spiritual, watercolor with gold accents. 
               Include sacred geometry and soft cosmic elements.`,
      size: '1024x1024',
      quality: 'standard',
      n: 1,
    });

    const imageUrl = imageResponse.data[0]?.url;
    if (!imageUrl) {
      throw new Error('Failed to generate image');
    }

    // In production, you'd want to save this image to your storage
    return imageUrl;
  } catch (error) {
    console.error('Image generation error:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Building the Conversation Flow

Here's how we maintain context and create meaningful oracle readings:

class OracleReadingSession {
  private messages: ChatCompletionMessageParam[] = [];

  constructor() {
    // Initialize with system prompt
    this.messages.push({
      role: 'system',
      content: `You are a wise and intuitive oracle card reader. You provide 
                compassionate, insightful guidance through oracle cards. When users 
                seek guidance, you naturally know when to draw cards or suggest 
                creating personalized ones. Always interpret cards in context of 
                the user's situation.`
    });
  }

  async processUserMessage(userInput: string): Promise<string> {
    // Add user message
    this.messages.push({
      role: 'user',
      content: userInput
    });

    // Process with our oracle agent
    const response = await processOracleReading(this.messages);

    // Add assistant response to history
    this.messages.push(response);

    return response.content || '';
  }

  getConversationHistory(): ChatCompletionMessageParam[] {
    return [...this.messages];
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling Streaming Responses

For a better user experience, implement streaming:

async function* streamOracleReading(messages: ChatCompletionMessageParam[]) {
  const stream = await openai.chat.completions.create({
    model: 'gpt-4-turbo-preview',
    messages: messages,
    tools: oracleTools,
    tool_choice: 'auto',
    stream: true,
  });

  for await (const chunk of stream) {
    const delta = chunk.choices[0]?.delta;

    if (delta?.content) {
      yield { type: 'content', data: delta.content };
    }

    if (delta?.tool_calls) {
      yield { type: 'tool_call', data: delta.tool_calls };
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

🎨 Advanced Features

Multi-Language Support

The system dynamically loads oracle cards based on user locale:

const loadOracleCardsByLocale = async (locale: string) => {
  const cardSets = {
    'en': await import('./cards/en.json'),
    'es': await import('./cards/es.json'),
    'ja': await import('./cards/ja.json'),
    // Add more languages
  };

  return cardSets[locale] || cardSets['en'];
};
Enter fullscreen mode Exit fullscreen mode

Contextual Card Interpretations

The AI considers the full conversation context when interpreting cards:

const enhancedSystemPrompt = `
  You are an experienced oracle card reader who:
  - Remembers previous cards drawn in the session
  - Connects card meanings to the user's specific situation
  - Offers practical advice alongside spiritual guidance
  - Recognizes patterns across multiple card draws
`;
Enter fullscreen mode Exit fullscreen mode

🚀 Performance Optimizations

Implementing Caching

Cache frequently drawn cards to reduce latency:

class OracleCardCache {
  private cache = new Map<string, { card: OracleCard; timestamp: number }>();
  private readonly TTL = 3600000; // 1 hour

  get(id: string): OracleCard | null {
    const cached = this.cache.get(id);
    if (cached && Date.now() - cached.timestamp < this.TTL) {
      return cached.card;
    }
    return null;
  }

  set(id: string, card: OracleCard): void {
    this.cache.set(id, { card, timestamp: Date.now() });
  }
}
Enter fullscreen mode Exit fullscreen mode

Rate Limiting and Error Handling

Implement proper error handling for production:

class OracleAPIClient {
  private retryCount = 3;
  private retryDelay = 1000;

  async makeRequest<T>(
    fn: () => Promise<T>,
    attempt = 1
  ): Promise<T> {
    try {
      return await fn();
    } catch (error) {
      if (attempt < this.retryCount) {
        await new Promise(resolve => 
          setTimeout(resolve, this.retryDelay * attempt)
        );
        return this.makeRequest(fn, attempt + 1);
      }
      throw error;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

🎯 Key Takeaways

  1. Function Calling is Powerful: OpenAI's function calling transforms LLMs into autonomous agents that can intelligently decide when and how to use external tools.

  2. Context Matters: Maintaining conversation history allows for more meaningful and personalized oracle readings.

  3. User Experience: Streaming responses and proper error handling create a smooth, professional experience.

  4. Extensibility: The architecture easily supports adding new card decks, languages, and features.

🔮 What's Next?

Some ideas for extending this system:

  • Add voice interactions for a more immersive experience
  • Implement card spread layouts (like Celtic Cross)
  • Create a learning system that adapts to user preferences
  • Build a community feature for sharing custom oracle cards

🌟 Try It Yourself

Experience the full Oracle Card reading system at Tarotap - Oracle Cards. Whether you're seeking guidance, exploring spirituality, or just curious about AI-powered divination, give it a try!


Have you built something interesting with OpenAI's function calling? I'd love to hear about your experiences in the comments! Drop a 🔮 if you found this helpful.

openai #ai #typescript #functioncalling #spiritualtech

Top comments (0)