16 KiB
Minimal Pistn API Integration Plan for ServiceBot
Date Created: 2025-11-06 Context: ServiceBot integration with Pistn after Claude OS knowledge export is complete Category: Architecture & Integration Status: Planning - Not Yet Implemented
Current State (November 2025)
✅ What's Complete:
- Claude OS Export System: Generic knowledge export to SQLite (format v1.0)
- ServiceBot Knowledge Integration: Embedded RAG engine with vector search
- ServiceBot MCP Servers: 4 servers with 7 tools (currently using stubs)
- JavaScript Widget: Production-ready chat modal for Pistn frontend
- Vision Documents: Claude OS Family established
⚠️ What's Stubbed:
PistnAPIMCPServer.get_dealer_info()- Returns fake dealer dataServicesMCPServer.list_services()- Returns hardcoded servicesAppointmentsMCPServer.check_availability()- Returns fake time slotsAppointmentsMCPServer.create_appointment()- Doesn't actually create appointments
🎯 The Problem:
Pistn has NO API endpoints. ServiceBot can't:
- Get real dealer hours, contact info
- List actual services with real prices
- Check real appointment availability (uses Pistn's complex scheduling logic)
- Create actual appointments (uses Pistn's business rules, validations, callbacks)
The Solution: Three-Phase Minimal API
Build a minimal, secure API in Pistn that ServiceBot can call to execute Pistn's business logic.
Phase 1: Test ServiceBot with Knowledge Only (Do This First!)
Goal: Validate export + chat widget work before building API
What to Test:
- Export knowledge from Claude OS
- Import into ServiceBot
- Add JavaScript widget to Pistn
- Test knowledge-only conversations
Commands:
# In Claude OS
/claude-os-export dealer_123 --output ./exports
# Copy to ServiceBot
cp claude-os/exports/dealer_123_*.db servicebot/data/dealer_123.db
# Configure ServiceBot .env
KNOWLEDGE_DB_PATH=data/dealer_123.db
ENABLE_KNOWLEDGE_SEARCH=true
KNOWLEDGE_USE_EMBEDDINGS=true
OPENAI_API_KEY=your_key
# Deploy ServiceBot
cd servicebot
docker-compose up -d
# Add widget to Pistn layout
# (see implementation below)
Test Conversations (knowledge-only):
- "What are your hours?" ✅ (from knowledge base)
- "How long does an oil change take?" ✅ (from knowledge base)
- "What's your cancellation policy?" ✅ (from knowledge base)
- "I want to book an appointment" ⚠️ (will fail gracefully - no API yet)
Success Criteria:
- Widget loads on Pistn
- Knowledge queries work
- Responses stream properly
- Graceful degradation when tools not available
Pistn Changes (Minimal):
# app/views/layouts/application.html.erb (or dealer layout)
<% if ENV['SERVICEBOT_ENABLED'] == 'true' %>
<script src="<%= ENV['SERVICEBOT_URL'] %>/static/servicebot-widget.js"></script>
<script>
ServiceBotWidget.init({
apiUrl: '<%= ENV['SERVICEBOT_URL'] %>',
dealerId: '<%= current_account.id if current_account.is_a?(DealerAccount) %>',
primaryColor: '<%= current_account.primary_bg if current_account.is_a?(DealerAccount) %>',
greeting: 'Hi! How can we help you today?'
});
</script>
<% end %>
Duration: 1-2 days (testing and validation)
Phase 2: Add Minimal Read-Only API (After Phase 1 Works)
Goal: Replace stubs with real Pistn data (no writes yet)
Implementation
2.1: Create API Controller
# pistn/app/controllers/api/v1/servicebot_controller.rb
module Api
module V1
class ServicebotController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :authenticate_servicebot
# GET /api/v1/servicebot/dealers/:id
def dealer_info
dealer = DealerAccount.find(params[:id])
render json: {
id: dealer.id,
name: dealer.name,
address: dealer.full_address,
city: dealer.city,
state: dealer.state,
zip: dealer.zip,
phone: dealer.phone,
email: dealer.email,
hours: build_hours_hash(dealer)
}
rescue ActiveRecord::RecordNotFound
render json: { error: 'Dealer not found' }, status: 404
end
# GET /api/v1/servicebot/dealers/:dealer_id/services
def services
dealer = DealerAccount.find(params[:dealer_id])
# Adjust based on Pistn's actual service model
services = dealer.oil_change_services || []
render json: services.map { |service| {
id: service.id,
name: service.name,
description: service.description || '',
price: service.price,
duration_minutes: service.duration_minutes || 60
}}
end
private
def authenticate_servicebot
api_key = request.headers['X-ServiceBot-API-Key']
unless api_key.present? && api_key == ENV['SERVICEBOT_API_KEY']
render json: { error: 'Unauthorized' }, status: 401
end
end
def build_hours_hash(dealer)
# Adjust based on how Pistn stores hours
{
monday: dealer.monday_hours || 'Closed',
tuesday: dealer.tuesday_hours || 'Closed',
wednesday: dealer.wednesday_hours || 'Closed',
thursday: dealer.thursday_hours || 'Closed',
friday: dealer.friday_hours || 'Closed',
saturday: dealer.saturday_hours || 'Closed',
sunday: dealer.sunday_hours || 'Closed'
}
end
end
end
end
2.2: Add Routes
# pistn/config/routes.rb
namespace :api do
namespace :v1 do
scope :servicebot do
get 'dealers/:id', to: 'servicebot#dealer_info'
get 'dealers/:dealer_id/services', to: 'servicebot#services'
end
end
end
2.3: Update ServiceBot MCP Servers
# servicebot/app/mcp_servers/pistn_api.py
class PistnAPIMCPServer:
def __init__(self):
self.base_url = settings.PISTN_API_URL # e.g., http://localhost:3000
self.api_key = settings.PISTN_API_KEY
self.client = httpx.AsyncClient(
base_url=self.base_url,
headers={
"X-ServiceBot-API-Key": self.api_key,
"Content-Type": "application/json"
},
timeout=10.0
)
async def get_dealer_info(self, dealer_id: str) -> Dict[str, Any]:
"""Get real dealer data from Pistn API."""
try:
response = await self.client.get(f"/api/v1/servicebot/dealers/{dealer_id}")
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Failed to get dealer info: {e}")
# Fallback to stub for graceful degradation
return self._stub_dealer_info(dealer_id)
# servicebot/app/mcp_servers/services.py
class ServicesMCPServer:
async def list_services(self, dealer_id: str) -> Dict[str, Any]:
"""Get real services from Pistn API."""
try:
response = await self.pistn_client.get(
f"/api/v1/servicebot/dealers/{dealer_id}/services"
)
response.raise_for_status()
services = response.json()
return {
"success": True,
"services": services,
"count": len(services)
}
except Exception as e:
logger.error(f"Failed to get services: {e}")
return {"success": False, "services": [], "count": 0}
2.4: Configuration
Pistn .env:
# ServiceBot Integration
SERVICEBOT_ENABLED=true
SERVICEBOT_URL=http://localhost:8000 # or production URL
SERVICEBOT_API_KEY=your_secure_random_key_here # Generate with: SecureRandom.hex(32)
ServiceBot .env:
# Pistn API Integration
PISTN_API_URL=http://localhost:3000 # or production Pistn URL
PISTN_API_KEY=same_key_as_above
Test:
# Test Pistn API directly
curl -H "X-ServiceBot-API-Key: your_key" \
http://localhost:3000/api/v1/servicebot/dealers/123
# Test in ServiceBot
# Should now return real dealer data instead of stubs
Duration: 2-3 days (implementation + testing)
Phase 3: Add Appointment Booking (After Phase 2 Works)
Goal: Enable full appointment booking through ServiceBot
3.1: Identify Pistn's Appointment Logic
Before implementing, answer these questions:
- What model handles appointments? (e.g.,
Appointment,ServiceAppointment) - What service/class creates appointments? (e.g.,
AppointmentCreationService) - What validates availability? (e.g.,
AppointmentAvailabilityService,BlockScheduler) - What are required fields? (customer info, vehicle, service, time)
- What validations exist? (blackout dates, capacity, dealer hours)
- What side effects happen? (emails, SMS, calendar updates)
3.2: Add Appointment Endpoints
# pistn/app/controllers/api/v1/servicebot_controller.rb
# POST /api/v1/servicebot/dealers/:dealer_id/appointments/check_availability
def check_availability
dealer = DealerAccount.find(params[:dealer_id])
date = Date.parse(params[:date])
service_id = params[:service_id]
# Use existing Pistn service (adjust to actual service name)
availability = AppointmentAvailabilityService.new(
dealer: dealer,
date: date,
service_id: service_id
).call
render json: {
success: true,
date: date,
slots: availability.map { |slot| {
time: slot.time.iso8601,
available: slot.available,
capacity: slot.capacity
}}
}
rescue => e
render json: { success: false, error: e.message }, status: 422
end
# POST /api/v1/servicebot/dealers/:dealer_id/appointments
def create_appointment
dealer = DealerAccount.find(params[:dealer_id])
# Use existing Pistn service (adjust to actual service name)
result = AppointmentCreationService.new(
dealer: dealer,
customer_name: params[:customer_name],
customer_email: params[:customer_email],
customer_phone: params[:customer_phone],
service_id: params[:service_id],
appointment_time: Time.parse(params[:appointment_time]),
vehicle_year: params.dig(:vehicle, :year),
vehicle_make: params.dig(:vehicle, :make),
vehicle_model: params.dig(:vehicle, :model),
notes: params[:notes]
).call
if result.success?
render json: {
success: true,
appointment: {
id: result.appointment.id,
confirmation_number: result.appointment.confirmation_number,
scheduled_time: result.appointment.scheduled_time.iso8601
}
}
else
render json: {
success: false,
errors: result.errors
}, status: 422
end
rescue => e
render json: { success: false, error: e.message }, status: 500
end
3.3: Add Routes
# pistn/config/routes.rb
namespace :api do
namespace :v1 do
scope :servicebot do
# ... existing routes ...
post 'dealers/:dealer_id/appointments/check_availability',
to: 'servicebot#check_availability'
post 'dealers/:dealer_id/appointments',
to: 'servicebot#create_appointment'
end
end
end
3.4: Update ServiceBot MCP Server
# servicebot/app/mcp_servers/appointments.py
class AppointmentsMCPServer:
async def check_availability(
self,
dealer_id: str,
date: str,
service_id: Optional[str] = None
) -> Dict[str, Any]:
"""Check real appointment availability via Pistn API."""
try:
response = await self.pistn_client.post(
f"/api/v1/servicebot/dealers/{dealer_id}/appointments/check_availability",
json={"date": date, "service_id": service_id}
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Availability check failed: {e}")
return {"success": False, "error": str(e)}
async def create_appointment(
self,
dealer_id: str,
customer_name: str,
customer_email: str,
customer_phone: str,
service_id: str,
appointment_time: str,
vehicle: Dict[str, str],
notes: Optional[str] = None
) -> Dict[str, Any]:
"""Create real appointment via Pistn API."""
try:
response = await self.pistn_client.post(
f"/api/v1/servicebot/dealers/{dealer_id}/appointments",
json={
"customer_name": customer_name,
"customer_email": customer_email,
"customer_phone": customer_phone,
"service_id": service_id,
"appointment_time": appointment_time,
"vehicle": vehicle,
"notes": notes
}
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Appointment creation failed: {e}")
return {"success": False, "error": str(e)}
Duration: 3-5 days (complex, needs thorough testing)
Security Considerations
API Key Generation
# Generate secure API key
SecureRandom.hex(32)
# => "a1b2c3d4e5f6..."
Rate Limiting (Optional but Recommended)
# Gemfile
gem 'rack-attack'
# config/initializers/rack_attack.rb
Rack::Attack.throttle('servicebot/ip', limit: 100, period: 1.minute) do |req|
if req.path.start_with?('/api/v1/servicebot')
req.ip
end
end
HTTPS in Production
- ServiceBot MUST use HTTPS (WebSocket requires it)
- Pistn API should use HTTPS for security
- Set
PISTN_API_URL=https://pistn.yourdomain.comin production
Testing Strategy
Phase 1 Testing
- Export knowledge from Claude OS
- Import into ServiceBot successfully
- Widget loads on Pistn page
- Can ask knowledge questions
- Streaming responses work
- Conversation persists in localStorage
Phase 2 Testing
- API authentication works
- Can get real dealer info
- Can list real services
- Graceful fallback if API fails
- Error handling works
Phase 3 Testing
- Can check availability for real dates
- Can create actual appointments
- Appointments appear in Pistn dashboard
- Customer receives confirmation
- All validations work (capacity, hours, etc.)
- Error messages are user-friendly
Deployment Checklist
ServiceBot Deployment
- Docker container running
- Knowledge DB imported
- HTTPS/SSL configured
- CORS configured for Pistn domain
- Environment variables set
- Widget file served at
/static/servicebot-widget.js
Pistn Deployment
- API controller added
- Routes configured
- API key set in
.env - Widget script added to layout
- ServiceBot enabled flag set
- CORS headers configured (if needed)
Monitoring & Maintenance
Logs to Watch
ServiceBot:
- WebSocket connections
- MCP tool calls
- API call failures
- Knowledge query performance
Pistn:
- ServiceBot API requests
- Authentication failures
- Appointment creation errors
Metrics to Track
- Conversations started
- Messages sent
- Knowledge queries performed
- Appointments booked via chat
- API response times
- Error rates
Future Enhancements
Post-MVP Features
- Customer authentication (lookup by email/phone)
- Appointment rescheduling
- Appointment cancellation
- Service history lookup
- Vehicle history integration
- Multi-language support
- Voice integration
- SMS integration
Knowledge Base Updates
- Sync
enhanced_documentation_service.rbknowledge to Claude OS - Regular export/import workflow
- Automated knowledge updates
Rollback Plan
If something breaks:
-
Phase 1 Issues: Disable widget
# .env SERVICEBOT_ENABLED=false -
Phase 2 Issues: ServiceBot falls back to stubs automatically
-
Phase 3 Issues: Disable appointment booking tool
# servicebot/app/mcp_servers/appointments.py self.enabled = False
Key Takeaways
✅ Start minimal - Test knowledge-only first ✅ Phased approach - Build API incrementally ✅ Reuse Pistn logic - Don't duplicate business rules ✅ Graceful degradation - Stubs as fallbacks ✅ Security first - API key authentication ✅ Test thoroughly - Each phase validated before next
Related Documentation
Next Action: Run Phase 1 to test ServiceBot with knowledge-only conversations!