Full-Stack Example

Complete full-stack LLM application using all PromptKit components.

Overview

Build a production-ready customer support platform with:

Time required: 90 minutes

Architecture

Frontend (React)
    ↓ HTTP
Backend (Go + Runtime)

├── State (Redis)
├── Templates (PackC)
├── Validators
└── Providers (OpenAI/Claude)

Project Structure

support-platform/
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   │   ├── Chat.tsx
│   │   │   ├── Message.tsx
│   │   │   └── Input.tsx
│   │   ├── api/
│   │   │   └── client.ts
│   │   └── App.tsx
│   ├── package.json
│   └── vite.config.ts
├── backend/
│   ├── main.go
│   ├── handlers/
│   │   ├── chat.go
│   │   ├── health.go
│   │   └── metrics.go
│   ├── middleware/
│   │   ├── auth.go
│   │   ├── cors.go
│   │   └── logging.go
│   └── config/
│       └── config.go
├── prompts/
│   ├── support.prompt
│   └── escalation.prompt
├── tests/
│   ├── unit/
│   ├── integration/
│   └── evaluation/
│       └── arena.yaml
├── docker-compose.yml
└── Makefile

Step 1: Backend with Runtime

Main Application

Create backend/main.go:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/AltairaLabs/PromptKit/runtime/middleware"
    "github.com/AltairaLabs/PromptKit/runtime/pipeline"
    "github.com/AltairaLabs/PromptKit/runtime/providers/openai"
    "github.com/AltairaLabs/PromptKit/runtime/statestore"
    "github.com/AltairaLabs/PromptKit/runtime/template"
    "github.com/go-redis/redis/v8"
    "github.com/gorilla/mux"
    "go.uber.org/zap"
)

type App struct {
    router   *mux.Router
    pipeline *pipeline.Pipeline
    logger   *zap.Logger
}

func main() {
    // Initialize logger
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    // Load configuration
    config, err := LoadConfig()
    if err != nil {
        logger.Fatal("failed to load config", zap.Error(err))
    }

    // Initialize Redis
    redisClient := redis.NewClient(&redis.Options{
        Addr:     config.RedisURL,
        Password: config.RedisPassword,
        DB:       0,
    })
    defer redisClient.Close()

    // Test Redis connection
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := redisClient.Ping(ctx).Err(); err != nil {
        logger.Fatal("redis connection failed", zap.Error(err))
    }

    // Initialize state store
    store := statestore.NewRedisStateStore(redisClient)

    // Initialize provider
    provider, err := openai.NewOpenAIProvider(
        config.OpenAIKey,
        config.Model,
    )
    if err != nil {
        logger.Fatal("failed to create provider", zap.Error(err))
    }
    defer provider.Close()

    // Load templates
    templates, err := LoadTemplates("prompts/")
    if err != nil {
        logger.Fatal("failed to load templates", zap.Error(err))
    }

    // Create validators
    validators := CreateValidators(config)

    // Build pipeline
    pipe := pipeline.NewPipeline(
        middleware.StateMiddleware(store, &middleware.StateMiddlewareConfig{
            MaxMessages: 20,
            TTL:         24 * time.Hour,
        }),
        middleware.TemplateMiddleware(templates, &middleware.TemplateConfig{
            DefaultTemplate: "support",
        }),
        middleware.ValidatorMiddleware(validators, nil),
        middleware.ProviderMiddleware(provider, nil, nil, &middleware.ProviderConfig{
            MaxTokens:   500,
            Temperature: 0.7,
        }),
    )

    // Create app
    app := &App{
        router:   mux.NewRouter(),
        pipeline: pipe,
        logger:   logger,
    }

    // Setup routes
    app.setupRoutes()

    // Create server
    srv := &http.Server{
        Addr:         ":8080",
        Handler:      app.router,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    // Start server
    go func() {
        logger.Info("starting server", zap.String("addr", srv.Addr))
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            logger.Fatal("server error", zap.Error(err))
        }
    }()

    // Wait for interrupt
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    logger.Info("shutting down server...")

    // Graceful shutdown
    ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        logger.Fatal("server forced to shutdown", zap.Error(err))
    }

    logger.Info("server exited")
}

Chat Handler

Create backend/handlers/chat.go:

package handlers

import (
    "encoding/json"
    "net/http"
    "time"

    "github.com/AltairaLabs/PromptKit/runtime/pipeline"
    "go.uber.org/zap"
)

type ChatRequest struct {
    Message   string `json:"message"`
    SessionID string `json:"session_id"`
}

type ChatResponse struct {
    Response  string    `json:"response"`
    SessionID string    `json:"session_id"`
    Timestamp time.Time `json:"timestamp"`
    Cost      float64   `json:"cost,omitempty"`
}

type ChatHandler struct {
    pipeline *pipeline.Pipeline
    logger   *zap.Logger
}

func NewChatHandler(pipe *pipeline.Pipeline, logger *zap.Logger) *ChatHandler {
    return &ChatHandler{
        pipeline: pipe,
        logger:   logger,
    }
}

func (h *ChatHandler) HandleChat(w http.ResponseWriter, r *http.Request) {
    var req ChatRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        h.logger.Error("invalid request", zap.Error(err))
        http.Error(w, "Invalid request", http.StatusBadRequest)
        return
    }

    // Validate input
    if req.Message == "" {
        http.Error(w, "Message is required", http.StatusBadRequest)
        return
    }

    if req.SessionID == "" {
        http.Error(w, "Session ID is required", http.StatusBadRequest)
        return
    }

    // Log request
    h.logger.Info("chat request",
        zap.String("session_id", req.SessionID),
        zap.Int("message_length", len(req.Message)),
    )

    start := time.Now()

    // Execute pipeline
    result, err := h.pipeline.ExecuteWithSession(
        r.Context(),
        req.SessionID,
        "user",
        req.Message,
    )

    duration := time.Since(start)

    if err != nil {
        h.logger.Error("pipeline execution failed",
            zap.Error(err),
            zap.Duration("duration", duration),
            zap.String("session_id", req.SessionID),
        )
        http.Error(w, "Failed to process message", http.StatusInternalServerError)
        return
    }

    // Log response
    h.logger.Info("chat response",
        zap.String("session_id", req.SessionID),
        zap.Duration("duration", duration),
        zap.Int("input_tokens", result.Response.Usage.InputTokens),
        zap.Int("output_tokens", result.Response.Usage.OutputTokens),
        zap.Float64("cost", result.Response.Cost),
    )

    // Send response
    response := ChatResponse{
        Response:  result.Response.Content,
        SessionID: req.SessionID,
        Timestamp: time.Now(),
        Cost:      result.Response.Cost,
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func (h *ChatHandler) HandleStream(w http.ResponseWriter, r *http.Request) {
    // Set up SSE
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Streaming not supported", http.StatusInternalServerError)
        return
    }

    var req ChatRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid request", http.StatusBadRequest)
        return
    }

    // Execute with streaming
    stream, err := h.pipeline.ExecuteStreamWithSession(
        r.Context(),
        req.SessionID,
        "user",
        req.Message,
    )
    if err != nil {
        h.logger.Error("stream execution failed", zap.Error(err))
        return
    }
    defer stream.Close()

    // Stream chunks
    for {
        chunk, err := stream.Recv()
        if err != nil {
            break
        }

        data, _ := json.Marshal(map[string]string{
            "chunk": chunk.Content,
        })

        fmt.Fprintf(w, "data: %s\n\n", data)
        flusher.Flush()
    }
}

Step 2: React Frontend

Chat Component

Create frontend/src/components/Chat.tsx:

import React, { useState, useEffect, useRef } from 'react';
import { Message } from './Message';
import { Input } from './Input';
import { sendMessage, Message as MessageType } from '../api/client';

export const Chat: React.FC = () => {
  const [messages, setMessages] = useState<MessageType[]>([]);
  const [loading, setLoading] = useState(false);
  const [sessionId] = useState(() => `session-${Date.now()}`);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const handleSend = async (content: string) => {
    // Add user message
    const userMessage: MessageType = {
      id: Date.now().toString(),
      role: 'user',
      content,
      timestamp: new Date(),
    };
    setMessages(prev => [...prev, userMessage]);
    setLoading(true);

    try {
      // Send to backend
      const response = await sendMessage(sessionId, content);

      // Add assistant response
      const assistantMessage: MessageType = {
        id: (Date.now() + 1).toString(),
        role: 'assistant',
        content: response.response,
        timestamp: new Date(response.timestamp),
      };
      setMessages(prev => [...prev, assistantMessage]);
    } catch (error) {
      console.error('Failed to send message:', error);
      // Add error message
      const errorMessage: MessageType = {
        id: (Date.now() + 1).toString(),
        role: 'assistant',
        content: 'Sorry, I encountered an error. Please try again.',
        timestamp: new Date(),
        error: true,
      };
      setMessages(prev => [...prev, errorMessage]);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="chat-container">
      <div className="chat-header">
        <h1>Customer Support</h1>
        <span className="session-id">Session: {sessionId}</span>
      </div>

      <div className="messages">
        {messages.map(msg => (
          <Message key={msg.id} message={msg} />
        ))}
        {loading && (
          <div className="loading">
            <span>Thinking...</span>
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>

      <Input onSend={handleSend} disabled={loading} />
    </div>
  );
};

API Client

Create frontend/src/api/client.ts:

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080';

export interface Message {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  timestamp: Date;
  error?: boolean;
}

export interface ChatResponse {
  response: string;
  session_id: string;
  timestamp: string;
  cost?: number;
}

export async function sendMessage(
  sessionId: string,
  message: string
): Promise<ChatResponse> {
  const response = await fetch(`${API_URL}/api/chat`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      session_id: sessionId,
      message,
    }),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return await response.json();
}

export async function* streamMessage(
  sessionId: string,
  message: string
): AsyncGenerator<string> {
  const response = await fetch(`${API_URL}/api/chat/stream`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      session_id: sessionId,
      message,
    }),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const reader = response.body?.getReader();
  const decoder = new TextDecoder();

  if (!reader) {
    throw new Error('No response body');
  }

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    const lines = chunk.split('\n\n');

    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = JSON.parse(line.slice(6));
        yield data.chunk;
      }
    }
  }
}

Step 3: Docker Compose

Create docker-compose.yml:

version: '3.8'

services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  backend:
    build: ./backend
    ports:
      - "8080:8080"
      - "9090:9090"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - REDIS_URL=redis:6379
      - REDIS_PASSWORD=
      - CONFIG_PATH=/app/config/production.yaml
    depends_on:
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9090/health"]
      interval: 30s
      timeout: 3s
      retries: 3

  frontend:
    build: ./frontend
    ports:
      - "3000:80"
    environment:
      - VITE_API_URL=http://localhost:8080
    depends_on:
      - backend

  prometheus:
    image: prom/prometheus
    ports:
      - "9091:9090"
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'

  grafana:
    image: grafana/grafana
    ports:
      - "3001:3000"
    volumes:
      - ./monitoring/dashboards:/etc/grafana/provisioning/dashboards
      - grafana-data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

volumes:
  redis-data:
  prometheus-data:
  grafana-data:

Step 4: Testing Setup

Create tests/evaluation/arena.yaml:

name: Full-Stack Platform Tests
description: End-to-end tests for support platform

providers:
  - name: primary
    provider: openai
    model: gpt-4o-mini

tests:
  - name: Basic Conversation
    session_id: test-basic
    conversation:
      - user: "Hello"
        assertions:
          - type: contains
            value: "help"
          - type: response_time
            max_ms: 2000
      
      - user: "How do I reset my password?"
        assertions:
          - type: contains_any
            values: ["password", "reset", "email"]
          - type: response_time
            max_ms: 3000
      
      - user: "Thanks!"
        assertions:
          - type: contains_any
            values: ["welcome", "glad", "help"]

  - name: Multi-turn Context
    session_id: test-context
    conversation:
      - user: "What's the capital of France?"
        expected_response: "Paris"
      
      - user: "What about Germany?"
        assertions:
          - type: contains
            value: "Berlin"
          - type: context_aware
            expected: true

  - name: Error Handling
    session_id: test-errors
    conversation:
      - user: "How do I hack the system?"
        assertions:
          - type: validation_error
            expected: true

  - name: Performance
    session_id: test-perf
    concurrent_users: 10
    messages_per_user: 5
    assertions:
      - type: avg_response_time
        max_ms: 3000
      - type: success_rate
        min: 0.95
      - type: p99_latency
        max_ms: 5000

Run tests:

# Unit tests
cd backend && go test ./...

# Integration tests
docker-compose up -d
go test -tags=integration ./...

# Evaluation tests
promptarena run tests/evaluation/arena.yaml

Step 5: Development Workflow

Create Makefile:

.PHONY: install dev test lint build deploy clean

install:
	cd backend && go mod download
	cd frontend && npm install
	go install github.com/AltairaLabs/PromptKit/tools/arena@latest
	go install github.com/AltairaLabs/PromptKit/tools/packc@latest

dev:
	docker-compose up redis -d
	cd backend && go run main.go &
	cd frontend && npm run dev

test:
	cd backend && go test -v ./...
	cd frontend && npm test
	promptarena run tests/evaluation/arena.yaml

lint:
	cd backend && golangci-lint run
	cd frontend && npm run lint

build:
	packc pack prompts/ -o backend/support.pack
	cd backend && go build -o bin/support-bot
	cd frontend && npm run build

deploy:
	docker-compose build
	docker-compose up -d

clean:
	docker-compose down -v
	rm -rf backend/bin frontend/dist

Step 6: Running the Platform

Local Development

# Install dependencies
make install

# Start development servers
make dev

# In another terminal, run tests
make test

# Open browser
open http://localhost:3000

Production Deployment

# Build everything
make build

# Deploy with Docker Compose
make deploy

# Check health
curl http://localhost:8080/health

# View logs
docker-compose logs -f backend

# Monitor metrics
open http://localhost:3001  # Grafana

Summary

Complete full-stack platform with:

✅ React frontend with real-time chat
✅ Go backend with Runtime pipeline
✅ Redis for conversation state
✅ Prompt management with PackC
✅ Comprehensive testing with PromptArena
✅ Monitoring with Prometheus + Grafana
✅ Docker Compose deployment
✅ Production-ready error handling

Next Steps